一口氣講完了Redis常用的數(shù)據(jù)結(jié)構(gòu)及應(yīng)用場景

一、概述

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

二、Redis常見應(yīng)用場景

三、Redis有哪些數(shù)據(jù)結(jié)構(gòu)

3.1 String字符串

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

  • 單值緩存
  • 對象緩存
  • 計數(shù)器
  • 分布式鎖

單值緩存

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格式數(shù)據(jù))

計數(shù)器

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

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

定時任務(wù)防止同一時刻重復(fù)執(zhí)行,可以在業(yè)務(wù)執(zhí)行代碼前使用分布式鎖控制。

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";

    /**
     * 每小時執(zhí)行一次
     */
    @Scheduled(cron = "0 0 0/1 * * ?")
    public void notify() {
        if (!lockService.grabLock(LOCK_KEY)) {
            log.info("[GlobalNotifyJob] 沒有拿到鎖, 停止操作......");
            return;
        }
        // 拿到鎖,開始執(zhí)行業(yè)務(wù)...
    }
}
  • 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哈希

??哈希典型應(yīng)用場景:

  • 緩存對象信息(帖子標題、摘要、作者信息)
  • 記錄帖子的點贊數(shù)、評論數(shù)和點擊數(shù)
  • 電商購物車
命令 描述
HSET key field value 存儲一個哈希表key的鍵值
HSETNX key field value 存儲一個不存儲的哈希表key的鍵值
HMSET key field value [field value...] 在一個哈希表key中存儲多個鍵值對
HGET key field value 獲取哈希表key對應(yīng)的field鍵值
HMGET key field value 批量獲取哈希表key中多個field鍵值
HDEL key field [field ...] 刪除哈希表key中多個field的鍵值
HLEN key 返回哈希表key中field的數(shù)量
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列表

??列表的典型應(yīng)用場景:

  • 文章列表
  • 微博和微信公眾號消息
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中指定區(qū)間內(nèi)的元素,區(qū)間以偏移量start和stop指定
LINSERT key  BEFORE|AFTER pivot element // 在元素element前后插入pivot
LREM key count element                  //根據(jù)參數(shù) COUNT 的值,移除列表中與參數(shù) VALUE 相等的元素 count > 0 : 從表頭開始向表尾搜索,移除與 VALUE 相等的元素,數(shù)量為 COUNT 
BLPOP  key  [key ...]  timeout          //從key列表表頭彈出一個元素,若列表中沒有元素,阻塞等待 timeout秒,如果timeout=0,一直阻塞等待
BRPOP  key  [key ...]  timeout          //從key列表表尾彈出一個元素,若列表中沒有元素,阻塞等待 timeout秒,如果timeout=0,一直阻塞等待

set集合

??列表的典型應(yīng)用場景:

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

抽獎場景:

  1. 用戶參與抽獎
# 將用戶10001加入商品a的參與池子中
SADD luckdraw:product:a 10001
  1. 查看參與商品a抽獎的所有用戶
SMEMBERS luckdraw:product:a
  1. 抽取1名幸運中獎?wù)?/li>
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有序集合

??列表的典型應(yīng)用場景:

  • 微博熱搜榜
  • 刷禮物實時排行榜
  • 博客社區(qū)本周熱議

Redis有序集合和集合一樣也是string類型元素的集合,且不允許重復(fù)的成員。不同的是每個元素都會關(guān)聯(lián)一個 double類型的分數(shù),Redis正是通過分數(shù)來為集合中的成員進行從小到大的排序。有序集合的成員是唯一的,但分數(shù)score卻可以重復(fù)。下面使用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的數(shù)量必須以numkeys參數(shù)指定,并將該并集 (結(jié)果集) 儲存到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值加上對應(yīng)的增量
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有序集合和對應(yīng)的命令之后,我們來實現(xiàn)本周熱議排行榜功能,博客的本周熱議主要的實現(xiàn)思路是:

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

畫圖分析:

最終實現(xiàn)效果:

Bitmaps位圖

??位圖的典型應(yīng)用場景:

  • 用戶連續(xù)簽到功能

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

Redis位圖命令基本命令

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

這里的offset,大家姑且當做用戶ID來看就可以了,那么究竟如何去實現(xiàn)用戶打卡功能呢,我們可以利用上面的setbit命令來實現(xiàn),setbit的作用說的直白就是:在你想要的位置操作字節(jié)值,比如說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

# 統(tǒng)計用戶1000在2月份簽到次數(shù)
BITCOUNT u:sign:1000:202302

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

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

示例代碼:

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

    private Jedis jedis = new Jedis();

    /**
     * 用戶簽到
     *
     * @param uid  用戶ID
     * @param date 日期
     * @return 之前的簽到狀態(tài)
     */
    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 當前的簽到狀態(tài)
     */
    public boolean checkSign(int uid, LocalDate date) {
        int offset = date.getDayOfMonth() - 1;
        return jedis.getbit(buildSignKey(uid, date), offset);
    }

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

    /**
     * 獲取當月連續(xù)簽到次數(shù)
     *
     * @param uid  用戶ID
     * @param date 日期
     * @return 當月連續(xù)簽到次數(shù)
     */
    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) {
            // 取低位連續(xù)不為0的個數(shù)即為連續(xù)簽到次數(shù),需考慮當天尚未簽到的情況
            long v = list.get(0) == null ? 0 : list.get(0);
            for (int i = 0; i < date.getDayOfMonth(); i++) {
                if (v >> 1 << 1 == v) {
                    // 低位為0且非當天說明連續(xù)簽到中斷了
                    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為簽到狀態(tài)的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("本月簽到次數(shù):" + count);
        }

        {   // getContinuousSignCount
            long count = kit.getContinuousSignCount(1000, today);
            System.out.println("連續(xù)簽到次數(shù):" + 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() ? "√" : "-"));
            }
        }
    }
}

運行結(jié)果:

您已簽到:2023-02-07
您已簽到:2023-02-07
本月簽到次數(shù):5
連續(xù)簽到次數(shù):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發(fā)布訂閱

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容