一口氣講完了Redis常用的數據結構及應用場景

一、概述

Redis是互聯網技術領域使用最爲廣泛的存儲中間件,它是Remote Dictionary Service(遠程字典服務)的首字母縮寫,Redis以其超高的性能、活躍的社區、詳細的文檔以及豐富的客戶端庫支持在開源中間件領域廣受好評,國內外很多大型互聯網都在使用Redis,比如:TwitterGithub、新浪微博、阿里巴巴、京東、Stack Overflow等,可以說,深入瞭解Redis應用和實踐,已成爲如今中高級後端加法繞不開的必備技能。

二、Redis常見應用場景

三、Redis有哪些數據結構

3.1 String字符串

🔥字符串典型的使用場景:

  • 單值緩存
  • 對象緩存
  • 計數器
  • 分佈式鎖

單值緩存

127.0.0.1:6379> set num 1
OK
127.0.0.1:6379> get num
"1"
127.0.0.1:6379>

單值緩存

SET user:1 value(json格式數據)

計數器

文章閱讀量、點贊量、評論量

127.0.0.1:6379> incr article:read:id1
(integer) 1
127.0.0.1:6379> incr article:read:id1
(integer) 2
127.0.0.1:6379> incr article:up:id1
(integer) 1
127.0.0.1:6379> incr article:up:id2
(integer) 1
127.0.0.1:6379> incr article:comment:id1
(integer) 1
127.0.0.1:6379> incr article:comment:id1
(integer) 2
127.0.0.1:6379>

分佈式鎖

  • setnx

定時任務防止同一時刻重複執行,可以在業務執行代碼前使用分佈式鎖控制。

127.0.0.1:6379> setnx job GlobalNotifyJob
(integer) 1
127.0.0.1:6379> get job
"GlobalNotifyJob"
127.0.0.1:6379> ttl job
(integer) -1
127.0.0.1:6379>

僞代碼如下:

@Slf4j
@Component
public class GlobalNotifyJob {

    private static final String LOCK_KEY = "redis_notify_lock";

    /**
     * 每小時執行一次
     */
    @Scheduled(cron = "0 0 0/1 * * ?")
    public void notify() {
        if (!lockService.grabLock(LOCK_KEY)) {
            log.info("[GlobalNotifyJob] 沒有拿到鎖, 停止操作......");
            return;
        }
        // 拿到鎖,開始執行業務...
    }
}
  • setex + 過期時間【SETNX KEY_NAME TIMEOUT VALUE】
127.0.0.1:6379> setex key1 60 value1
OK
127.0.0.1:6379> ttl key1
(integer) 53
127.0.0.1:6379> get key1
"value1"
127.0.0.1:6379>

hash哈希

🔥哈希典型應用場景:

  • 緩存對象信息(帖子標題、摘要、作者信息)
  • 記錄帖子的點贊數、評論數和點擊數
  • 電商購物車
命令 描述
HSET key field value 存儲一個哈希表key的鍵值
HSETNX key field value 存儲一個不存儲的哈希表key的鍵值
HMSET key field value [field value...] 在一個哈希表key中存儲多個鍵值對
HGET key field value 獲取哈希表key對應的field鍵值
HMGET key field value 批量獲取哈希表key中多個field鍵值
HDEL key field [field ...] 刪除哈希表key中多個field的鍵值
HLEN key 返回哈希表key中field的數量
HGETALL key 返回哈希表key中所有的鍵值
127.0.0.1:6379> hmset user:1 name austin age 25 address guangzhou balance 6888
OK
127.0.0.1:6379> hget user:1 name
"austin"
127.0.0.1:6379> hget user:1 balance
"6888"
127.0.0.1:6379> hmget user:1 age address
1) "25"
2) "guangzhou"
127.0.0.1:6379> hlen user:1
(integer) 4
127.0.0.1:6379> hgetall user:1
1) "name"
2) "austin"
3) "age"
4) "25"
5) "address"
6) "guangzhou"
7) "balance"
8) "6888"
127.0.0.1:6379>

list列表

🔥列表的典型應用場景:

  • 文章列表
  • 微博和微信公衆號消息
Stack(棧FILO) = LPUSH + LPOP 
Queue(隊列FIFO)= LPUSH + RPOP 
Blocking MQ(阻塞隊列)= LPUSH + BRPOP

LPUSH  key  value [value ...]       // 將一個或多個值value插入到key列表的表頭(最左邊)
RPUSH  key  value [value ...]       // 將一個或多個值value插入到key列表的表尾(最右邊)
LPOP  key                   // 移除並返回key列表的頭元素
RPOP  key                   // 移除並返回key列表的尾元素
LRANGE  key  start  stop        // 返回列表key中指定區間內的元素,區間以偏移量start和stop指定
LINSERT key  BEFORE|AFTER pivot element // 在元素element前後插入pivot
LREM key count element                  //根據參數 COUNT 的值,移除列表中與參數 VALUE 相等的元素 count > 0 : 從表頭開始向表尾搜索,移除與 VALUE 相等的元素,數量爲 COUNT 
BLPOP  key  [key ...]  timeout          //從key列表表頭彈出一個元素,若列表中沒有元素,阻塞等待 timeout秒,如果timeout=0,一直阻塞等待
BRPOP  key  [key ...]  timeout          //從key列表表尾彈出一個元素,若列表中沒有元素,阻塞等待 timeout秒,如果timeout=0,一直阻塞等待

set集合

🔥列表的典型應用場景:

  • 抽獎
  • 微博點贊,收藏,標籤
  • 共同好友

抽獎場景:

  1. 用戶參與抽獎
# 將用戶10001加入商品a的參與池子中
SADD luckdraw:product:a 10001
  1. 查看參與商品a抽獎的所有用戶
SMEMBERS luckdraw:product:a
  1. 抽取1名幸運中獎者
SPOP luckdraw:product:a 1

共同好友場景:

用戶1的好友爲:3,4,8
用戶2的好友爲:4,5,11

取交集,獲取用戶1和用戶2的共同好友,爲用戶4。

127.0.0.1:6379> sadd user_1 2 3 4
(integer) 3
127.0.0.1:6379> sadd user_2 4 5 7
(integer) 3
127.0.0.1:6379> sinter user_1 user_2
1) "4"
127.0.0.1:6379>

sorted set有序集合

🔥列表的典型應用場景:

  • 微博熱搜榜
  • 刷禮物實時排行榜
  • 博客社區本週熱議

Redis有序集合和集合一樣也是string類型元素的集合,且不允許重複的成員。不同的是每個元素都會關聯一個 double類型的分數,Redis正是通過分數來爲集合中的成員進行從小到大的排序。有序集合的成員是唯一的,但分數score卻可以重複。下面使用redis-cli實踐Redis有序集合命令:

zset幾個基本命令:

命令 說明
zrange key start stop [WITHSCORES] 將集合元素依照順序值升序排序再輸出,startstop限制遍歷的限制範圍
zincrby key increment member 有序集key的成員memberscore值加上增量increment
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] 計算給定的一個或多個有序集的並集,其中給定key的數量必須以numkeys參數指定,並將該並集 (結果集) 儲存到destination
127.0.0.1:6379[3]> zadd zsetofpost 89 post:1
(integer) 1
127.0.0.1:6379[3]> zadd zsetofpost 123 post:2
(integer) 1
127.0.0.1:6379[3]> zadd zsetofpost 32 post:3
(integer) 1
127.0.0.1:6379[3]> zadd zsetofpost 432 post:4
(integer) 1
127.0.0.1:6379[3]> zadd zsetofpost 128 post:5
(integer) 1

#升序排序
127.0.0.1:6379[3]> zrange zsetofpost 0 -1 withscores
 1) "post:3"
 2) "32"
 3) "post:1"
 4) "89"
 5) "post:2"
 6) "123"
 7) "post:5"
 8) "128"
 9) "post:4"
10) "432"

#降序排序
127.0.0.1:6379[3]> zrevrange zsetofpost 0 -1 withscores
 1) "post:4"
 2) "432"
 3) "post:5"
 4) "128"
 5) "post:2"
 6) "123"
 7) "post:1"
 8) "89"
 9) "post:3"
10) "32"

#有序集合某個元素的score值加上對應的增量
127.0.0.1:6379[3]> zincrby zsetofpost 40 post:1
"129"
127.0.0.1:6379[3]> zincrby zsetofpost 500 post:3
"532"
127.0.0.1:6379[3]> zrange zsetofpost 0 -1 withscores
 1) "post:2"
 2) "123"
 3) "post:5"
 4) "128"
 5) "post:1"
 6) "129"
 7) "post:4"
 8) "432"
 9) "post:3"
10) "532"

簡單認識了Redis有序集合和對應的命令之後,我們來實現本週熱議排行榜功能,博客的本週熱議主要的實現思路是:

  1. 庫獲取最近 7 天的所有文章(或者加多一個條件:評論數量大於 0)。
  2. 把文章的評論數量作爲有序集合的分數score,文章的ID作爲key存儲到zset中,當有人發表評論的時候,直接使用命令加一,並重新計算得到排行榜。
  3. 本週熱議上有標題和評論數量,因此,我們還需要把文章的基本信息存儲到Redis中,這樣得到文章的ID之後,我們再從緩存中得到標題等信息,這裏我們可以使用hash的結構來存儲文章的信息。
  4. 因爲是本週熱議,如果文章發表超過 7 天了之後就會失效,所以我們可以給文章的有序集合一個有效時間。超過 7 天之後就自動刪除緩存。

畫圖分析:

最終實現效果:

Bitmaps位圖

🔥位圖的典型應用場景:

  • 用戶連續簽到功能

很多社區、博客平臺其實都有每日簽到模塊,一開始看到這個模塊需求的時候,很多人第一反應是利用MySQL來實現,創建一個簽到表,記錄用戶ID和簽到時間,然後統計的時候從數據庫中取出來然後聚合計算,這樣設計其實存在弊端,如我們想要做一些複雜的功能就不是太方便了,或者說不是太高性能了,比如,今天是連續簽到的第幾天,在一定時間內連續簽到了多少天。另外一方面,如果按 100 萬用戶量級來計算,一個用戶每年可以產生 365 條記錄,100 萬用戶的所有簽到記錄那就有點恐怖了,查詢計算速度也會越來越慢。其實RedisBitmaps位圖操作非常適合處理每日簽到功能場景,因爲Bit的值爲0或者1,位圖的每一位代表一天的簽到,1表示已籤,0表示未籤。 考慮到每月初需要重置連續簽到次數,最簡單的方式是按用戶每月存一條簽到數據(也可以每年存一條數據)。Key的格式爲u:sign:uid:yyyyMM,Value則採用長度爲4個字節(32位)的位圖(最大月份只有31天)。

Redis位圖命令基本命令

命令 說明
SETBIT key offset value 對key所儲存的字符串值,設置或清除指定偏移量上的位(bit)
BITPOS key bit [start] [end] 查詢指定字節區間第一個被設置成1的bit位的位置
GETBIT key offset 查詢指定偏移位置的bit值
BITCOUNT key [start end] 統計指定字節區間bit爲1的數量
GETBIT key offset 查詢指定偏移位置的bit值
BITFIELD key offset 查詢指定偏移位置的bit值

這裏的offset,大家姑且當做用戶ID來看就可以了,那麼究竟如何去實現用戶打卡功能呢,我們可以利用上面的setbit命令來實現,setbit的作用說的直白就是:在你想要的位置操作字節值,比如說u:sign:1000:202302表示ID=1000的用戶在2023年2月7號簽到記錄。

# 用戶1000在2023年2月7號簽到
SETBIT u:sign:1000:202302 6 1 # 偏移量是從0開始,所以要把7減1

# 檢查用戶1000在2023年2月7號是否簽到
GETBIT u:sign:1000:202302 6   # 偏移量是從0開始,所以要把7減1

# 統計用戶1000在2月份簽到次數
BITCOUNT u:sign:1000:202302

# 獲取2月份前28天的簽到數據
BITFIELD u:sign:1000:202302 get u28 0

# 獲取2月份首次簽到日期
BITPOS u:sign:1000:202302 1  # 返回的首次簽到的偏移量,加上1即爲當月的某一天

示例代碼:

/**
 * 基於Redis位圖的用戶簽到功能工具實現類
 *
 * @author: austin
 * @since: 2023/2/7 1:50
 */
public class UserSignKit {

    private Jedis jedis = new Jedis();

    /**
     * 用戶簽到
     *
     * @param uid  用戶ID
     * @param date 日期
     * @return 之前的簽到狀態
     */
    public boolean doSign(int uid, LocalDate date) {
        int offset = date.getDayOfMonth() - 1;
        return jedis.setbit(buildSignKey(uid, date), offset, true);
    }

    /**
     * 檢查用戶是否簽到
     *
     * @param uid  用戶ID
     * @param date 日期
     * @return 當前的簽到狀態
     */
    public boolean checkSign(int uid, LocalDate date) {
        int offset = date.getDayOfMonth() - 1;
        return jedis.getbit(buildSignKey(uid, date), offset);
    }

    /**
     * 獲取用戶簽到次數
     *
     * @param uid  用戶ID
     * @param date 日期
     * @return 當前的簽到次數
     */
    public long getSignCount(int uid, LocalDate date) {
        return jedis.bitcount(buildSignKey(uid, date));
    }

    /**
     * 獲取當月連續簽到次數
     *
     * @param uid  用戶ID
     * @param date 日期
     * @return 當月連續簽到次數
     */
    public long getContinuousSignCount(int uid, LocalDate date) {
        int signCount = 0;
        String type = String.format("u%d", date.getDayOfMonth());
        List<Long> list = jedis.bitfield(buildSignKey(uid, date), "GET", type, "0");
        if (list != null && list.size() > 0) {
            // 取低位連續不爲0的個數即爲連續簽到次數,需考慮當天尚未簽到的情況
            long v = list.get(0) == null ? 0 : list.get(0);
            for (int i = 0; i < date.getDayOfMonth(); i++) {
                if (v >> 1 << 1 == v) {
                    // 低位爲0且非當天說明連續簽到中斷了
                    if (i > 0) {
                        break;
                    }
                } else {
                    signCount += 1;
                }
                v >>= 1;
            }
        }
        return signCount;
    }

    /**
     * 獲取當月首次簽到日期
     *
     * @param uid  用戶ID
     * @param date 日期
     * @return 首次簽到日期
     */
    public LocalDate getFirstSignDate(int uid, LocalDate date) {
        long pos = jedis.bitpos(buildSignKey(uid, date), true);
        return pos < 0 ? null : date.withDayOfMonth((int) (pos + 1));
    }

    /**
     * 獲取當月簽到情況
     *
     * @param uid  用戶ID
     * @param date 日期
     * @return Key爲簽到日期,Value爲簽到狀態的Map
     */
    public Map<String, Boolean> getSignInfo(int uid, LocalDate date) {
        Map<String, Boolean> signMap = new HashMap<>(date.getDayOfMonth());
        String type = String.format("u%d", date.lengthOfMonth());
        List<Long> list = jedis.bitfield(buildSignKey(uid, date), "GET", type, "0");
        if (list != null && list.size() > 0) {
            // 由低位到高位,爲0表示未籤,爲1表示已籤
            long v = list.get(0) == null ? 0 : list.get(0);
            for (int i = date.lengthOfMonth(); i > 0; i--) {
                LocalDate d = date.withDayOfMonth(i);
                signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v);
                v >>= 1;
            }
        }
        return signMap;
    }

    private static String formatDate(LocalDate date) {
        return formatDate(date, "yyyyMM");
    }

    private static String formatDate(LocalDate date, String pattern) {
        return date.format(DateTimeFormatter.ofPattern(pattern));
    }

    private static String buildSignKey(int uid, LocalDate date) {
        return String.format("u:sign:%d:%s", uid, formatDate(date));
    }

    public static void main(String[] args) {
        UserSignKit kit = new UserSignKit();
        LocalDate today = LocalDate.now();

        {   // doSign
            boolean signed = kit.doSign(1000, today);
            if (signed) {
                System.out.println("您已簽到:" + formatDate(today, "yyyy-MM-dd"));
            } else {
                System.out.println("簽到完成:" + formatDate(today, "yyyy-MM-dd"));
            }
        }

        {   // checkSign
            boolean signed = kit.checkSign(1000, today);
            if (signed) {
                System.out.println("您已簽到:" + formatDate(today, "yyyy-MM-dd"));
            } else {
                System.out.println("尚未簽到:" + formatDate(today, "yyyy-MM-dd"));
            }
        }

        {   // getSignCount
            long count = kit.getSignCount(1000, today);
            System.out.println("本月簽到次數:" + count);
        }

        {   // getContinuousSignCount
            long count = kit.getContinuousSignCount(1000, today);
            System.out.println("連續簽到次數:" + count);
        }

        {   // getFirstSignDate
            LocalDate date = kit.getFirstSignDate(1000, today);
            System.out.println("本月首次簽到:" + formatDate(date, "yyyy-MM-dd"));
        }

        {   // getSignInfo
            System.out.println("當月簽到情況:");
            Map<String, Boolean> signInfo = new TreeMap<>(kit.getSignInfo(1000, today));
            for (Map.Entry<String, Boolean> entry : signInfo.entrySet()) {
                System.out.println(entry.getKey() + ": " + (entry.getValue() ? "√" : "-"));
            }
        }
    }
}

運行結果:

您已簽到:2023-02-07
您已簽到:2023-02-07
本月簽到次數:5
連續簽到次數:3
本月首次簽到:2023-02-02
當月簽到情況:
2023-02-01: -
2023-02-02: √
2023-02-03: √
2023-02-04: √
2023-02-05: -
2023-02-06: √
2023-02-07: √
2023-02-08: -
2023-02-09: -
2023-02-10: -
2023-02-11: -
2023-02-12: -
2023-02-13: -
2023-02-14: -
2023-02-15: -
2023-02-16: -
2023-02-17: -
2023-02-18: -
2023-02-19: -
2023-02-20: -
2023-02-21: -
2023-02-22: -
2023-02-23: -
2023-02-24: -
2023-02-25: -
2023-02-26: -
2023-02-27: -
2023-02-28: -

Redis發佈訂閱

Redis提供了發佈訂閱功能,可以用於消息的傳輸,Redis的發佈訂閱機制包括三個部分:發佈者訂閱者Channel。發佈者和訂閱者都是Redis客戶端,Channel則爲Redis服務器端,發佈者將消息發送到某個的頻道,訂閱了這個頻道的訂閱者就能接收到這條消息。Redis的這種發佈訂閱機制與基於主題的發佈訂閱類似,Channel相當於主題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章