天天看點

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

作者:小林coding

計算機八股文網(作業系統、計算機網絡、計算機組成、MySQL、Redis):https://xiaolincoding.com

大家好,我是小林。

我們都知道 Redis 提供了豐富的資料類型,常見的有五種:String(字元串),Hash(哈希),List(清單),Set(集合)、Zset(有序集合)。

随着 Redis 版本的更新,後面又支援了四種資料類型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。

每種資料對象都各自的應用場景,你能說出它們各自的應用場景嗎?

面試過程中,這個問題也很常被問到,又比如會舉例一個應用場景來問你,讓你說使用哪種 Redis 資料類型來實作。

是以,這次我們就來學習 Redis 資料類型的使用以及應用場景。

PS:你可以自己本機安裝 Redis 或者通過 Redis 官網提供的線上 Redis 環境 來敲指令。
2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

String

介紹

String 是最基本的 key-value 結構,key 是唯一辨別,value 是具體的值,value其實不僅是字元串, 也可以是數字(整數或浮點數),value 最多可以容納的資料長度是

512M

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

内部實作

String 類型的底層的資料結構實作主要是 int 和 SDS(簡單動态字元串)。

SDS 和我們認識的 C 字元串不太一樣,之是以沒有使用 C 語言的字元串表示,因為 SDS 相比于 C 的原生字元串:

  • SDS 不僅可以儲存文本資料,還可以儲存二進制資料。因為

    SDS

    使用

    len

    屬性的值而不是空字元來判斷字元串是否結束,并且 SDS 的所有 API 都會以處理二進制的方式來處理 SDS 存放在

    buf[]

    數組裡的資料。是以 SDS 不光能存放文本資料,而且能儲存圖檔、音頻、視訊、壓縮檔案這樣的二進制資料。
  • **SDS 擷取字元串長度的時間複雜度是 O(1)**。因為 C 語言的字元串并不記錄自身長度,是以擷取長度的複雜度為 O(n);而 SDS 結構裡用

    len

    屬性記錄了字元串長度,是以複雜度為

    O(1)

  • Redis 的 SDS API 是安全的,拼接字元串不會造成緩沖區溢出。因為 SDS 在拼接字元串之前會檢查 SDS 空間是否滿足要求,如果空間不夠會自動擴容,是以不會導緻緩沖區溢出的問題。

字元串對象的内部編碼(encoding)有 3 種 :int、raw和 embstr。

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

如果一個字元串對象儲存的是整數值,并且這個整數值可以用

long

類型來表示,那麼字元串對象會将整數值儲存在字元串對象結構的

ptr

屬性裡面(将

void*

轉換成 long),并将字元串對象的編碼設定為

int

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

如果字元串對象儲存的是一個字元串,并且這個字元申的長度小于等于 32 位元組(redis 2.+版本),那麼字元串對象将使用一個簡單動态字元串(SDS)來儲存這個字元串,并将對象的編碼設定為

embstr

embstr

編碼是專門用于儲存短字元串的一種優化編碼方式:

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

如果字元串對象儲存的是一個字元串,并且這個字元串的長度大于 32 位元組(redis 2.+版本),那麼字元串對象将使用一個簡單動态字元串(SDS)來儲存這個字元串,并将對象的編碼設定為

raw

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

注意,embstr 編碼和 raw 編碼的邊界在 redis 不同版本中是不一樣的:

  • redis 2.+ 是 32 位元組
  • redis 3.0-4.0 是 39 位元組
  • redis 5.0 是 44 位元組

可以看到

embstr

raw

編碼都會使用

SDS

來儲存值,但不同之處在于

embstr

會通過一次記憶體配置設定函數來配置設定一塊連續的記憶體空間來儲存

redisObject

SDS

,而

raw

編碼會通過調用兩次記憶體配置設定函數來分别配置設定兩塊空間來儲存

redisObject

SDS

。Redis這樣做會有很多好處:

  • embstr

    編碼将建立字元串對象所需的記憶體配置設定次數從

    raw

    編碼的兩次降低為一次;
  • 釋放

    embstr

    編碼的字元串對象同樣隻需要調用一次記憶體釋放函數;
  • 因為

    embstr

    編碼的字元串對象的所有資料都儲存在一塊連續的記憶體裡面可以更好的利用 CPU 緩存提升性能。

但是 embstr 也有缺點的:

  • 如果字元串的長度增加需要重新配置設定記憶體時,整個redisObject和sds都需要重新配置設定空間,是以embstr編碼的字元串對象實際上是隻讀的,redis沒有為embstr編碼的字元串對象編寫任何相應的修改程式。當我們對embstr編碼的字元串對象執行任何修改指令(例如append)時,程式會先将對象的編碼從embstr轉換成raw,然後再執行修改指令。

常用指令

普通字元串的基本操作:

# 設定 key-value 類型的值
> SET name lin
OK
# 根據 key 獲得對應的 value
> GET name
"lin"
# 判斷某個 key 是否存在
> EXISTS name
(integer) 1
# 傳回 key 所儲存的字元串值的長度
> STRLEN name
(integer) 3
# 删除某個 key 對應的值
> DEL name
(integer) 1
           

批量設定 :

# 批量設定 key-value 類型的值
> MSET key1 value1 key2 value2 
OK
# 批量擷取多個 key 對應的 value
> MGET key1 key2 
1) "value1"
2) "value2"
           

計數器(字元串的内容為整數的時候可以使用):

# 設定 key-value 類型的值
> SET number 0
OK
# 将 key 中儲存的數字值增一
> INCR number
(integer) 1
# 将key中存儲的數字值加 10
> INCRBY number 10
(integer) 11
# 将 key 中儲存的數字值減一
> DECR number
(integer) 10
# 将key中存儲的數字值鍵 10
> DECRBY number 10
(integer) 0
           

過期(預設為永不過期):

# 設定 key 在 60 秒後過期(該方法是針對已經存在的key設定過期時間)
> EXPIRE name  60 
(integer) 1
# 檢視資料還有多久過期
> TTL name 
(integer) 51

#設定 key-value 類型的值,并設定該key的過期時間為 60 秒
> SET key  value EX 60
OK
> SETEX key  60 value
OK
           

不存在就插入:

# 不存在就插入(not exists)
>SETNX key value
(integer) 1
           

應用場景

緩存對象

使用 String 來緩存對象有兩種方式:

  • 直接緩存整個對象的 JSON,指令例子:

    SET user:1 '{"name":"xiaolin", "age":18}'

  • 采用将 key 進行分離為 user:ID:屬性,采用 MSET 存儲,用 MGET 擷取各屬性值,指令例子:

    MSET user:1:name xiaolin user:1:age 18 user:2:name xiaomei user:2:age 20

正常計數

因為 Redis 處理指令是單線程,是以執行指令的過程是原子的。是以 String 資料類型适合計數場景,比如計算通路次數、點贊、轉發、庫存數量等等。

比如計算文章的閱讀量:

# 初始化文章的閱讀量
> SET aritcle:readcount:1001 0
OK
#閱讀量+1
> INCR aritcle:readcount:1001
(integer) 1
#閱讀量+1
> INCR aritcle:readcount:1001
(integer) 2
#閱讀量+1
> INCR aritcle:readcount:1001
(integer) 3
# 擷取對應文章的閱讀量
> GET aritcle:readcount:1001
"3"
           

分布式鎖

SET 指令有個 NX 參數可以實作「key不存在才插入」,可以用它來實作分布式鎖:

  • 如果 key 不存在,則顯示插入成功,可以用來表示加鎖成功;
  • 如果 key 存在,則會顯示插入失敗,可以用來表示加鎖失敗。

一般而言,還會對分布式鎖加上過期時間,分布式鎖的指令如下:

SET lock_key unique_value NX PX 10000
           
  • lock_key 就是 key 鍵;
  • unique_value 是用戶端生成的唯一的辨別;
  • NX 代表隻在 lock_key 不存在時,才對 lock_key 進行設定操作;
  • PX 10000 表示設定 lock_key 的過期時間為 10s,這是為了避免用戶端發生異常而無法釋放鎖。

而解鎖的過程就是将 lock_key 鍵删除,但不能亂删,要保證執行操作的用戶端就是加鎖的用戶端。是以,解鎖的時候,我們要先判斷鎖的 unique_value 是否為加鎖用戶端,是的話,才将 lock_key 鍵删除。

可以看到,解鎖是有兩個操作,這時就需要 Lua 腳本來保證解鎖的原子性,因為 Redis 在執行 Lua 腳本時,可以以原子性的方式執行,保證了鎖釋放操作的原子性。

// 釋放鎖時,先比較 unique_value 是否相等,避免鎖的誤釋放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
           

這樣一來,就通過使用 SET 指令和 Lua 腳本在 Redis 單節點上完成了分布式鎖的加鎖和解鎖。

共享 Session 資訊

通常我們在開發背景管理系統時,會使用 Session 來儲存使用者的會話(登入)狀态,這些 Session 資訊會被儲存在伺服器端,但這隻适用于單系統應用,如果是分布式系統此模式将不再适用。

例如使用者一的 Session 資訊被存儲在伺服器一,但第二次通路時使用者一被配置設定到伺服器二,這個時候伺服器并沒有使用者一的 Session 資訊,就會出現需要重複登入的問題,問題在于分布式系統每次會把請求随機配置設定到不同的伺服器。

分布式系統單獨存儲 Session 流程圖:

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

是以,我們需要借助 Redis 對這些 Session 資訊進行統一的存儲和管理,這樣無論請求發送到那台伺服器,伺服器都會去同一個 Redis 擷取相關的 Session 資訊,這樣就解決了分布式系統下 Session 存儲的問題。

分布式系統使用同一個 Redis 存儲 Session 流程圖:

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

List

介紹

List 清單是簡單的字元串清單,按照插入順序排序,可以從頭部或尾部向 List 清單添加元素。

清單的最大長度為

2^32 - 1

,也即每個清單支援超過

40 億

個元素。

内部實作

List 類型的底層資料結構是由雙向連結清單或壓縮清單實作的:

  • 如果清單的元素個數小于

    512

    個(預設值,可由

    list-max-ziplist-entries

    配置),清單每個元素的值都小于

    64

    位元組(預設值,可由

    list-max-ziplist-value

    配置),Redis 會使用壓縮清單作為 List 類型的底層資料結構;
  • 如果清單的元素不滿足上面的條件,Redis 會使用雙向連結清單作為 List 類型的底層資料結構;

但是在 Redis 3.2 版本之後,List 資料類型底層資料結構就隻由 quicklist 實作了,替代了雙向連結清單和壓縮清單。

常用指令

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景
# 将一個或多個值value插入到key清單的表頭(最左邊),最後的值在最前面
LPUSH key value [value ...] 
# 将一個或多個值value插入到key清單的表尾(最右邊)
RPUSH key value [value ...]
# 移除并傳回key清單的頭元素
LPOP key     
# 移除并傳回key清單的尾元素
RPOP key 

# 傳回清單key中指定區間内的元素,區間以偏移量start和stop指定,從0開始
LRANGE key start stop

# 從key清單表頭彈出一個元素,沒有就阻塞timeout秒,如果timeout=0則一直阻塞
BLPOP key [key ...] timeout
# 從key清單表尾彈出一個元素,沒有就阻塞timeout秒,如果timeout=0則一直阻塞
BRPOP key [key ...] timeout
           

應用場景

消息隊列

消息隊列在存取消息時,必須要滿足三個需求,分别是消息保序、處理重複的消息和保證消息可靠性。

Redis 的 List 和 Stream 兩種資料類型,就可以滿足消息隊列的這三個需求。我們先來了解下基于 List 的消息隊列實作方法,後面在介紹 Stream 資料類型時候,在詳細說說 Stream。

1、如何滿足消息保序需求?

List 本身就是按先進先出的順序對資料進行存取的,是以,如果使用 List 作為消息隊列儲存消息的話,就已經能滿足消息保序的需求了。

List 可以使用 LPUSH + RPOP (或者反過來,RPUSH+LPOP)指令實作消息隊列。

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景
  • 生産者使用

    LPUSH key value[value...]

    将消息插入到隊列的頭部,如果 key 不存在則會建立一個空的隊列再插入消息。
  • 消費者使用

    RPOP key

    依次讀取隊列的消息,先進先出。

不過,在消費者讀取資料時,有一個潛在的性能風險點。

在生産者往 List 中寫入資料時,List 并不會主動地通知消費者有新消息寫入,如果消費者想要及時處理消息,就需要在程式中不停地調用

RPOP

指令(比如使用一個while(1)循環)。如果有新消息寫入,RPOP指令就會傳回結果,否則,RPOP指令傳回空值,再繼續循環。

是以,即使沒有新消息寫入List,消費者也要不停地調用 RPOP 指令,這就會導緻消費者程式的 CPU 一直消耗在執行 RPOP 指令上,帶來不必要的性能損失。

為了解決這個問題,Redis提供了 BRPOP 指令。BRPOP指令也稱為阻塞式讀取,用戶端在沒有讀到隊列資料時,自動阻塞,直到有新的資料寫入隊列,再開始讀取新資料。和消費者程式自己不停地調用RPOP指令相比,這種方式能節省CPU開銷。

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

2、如何處理重複的消息?

消費者要實作重複消息的判斷,需要 2 個方面的要求:

  • 每個消息都有一個全局的 ID。
  • 消費者要記錄已經處理過的消息的 ID。當收到一條消息後,消費者程式就可以對比收到的消息 ID 和記錄的已處理過的消息 ID,來判斷目前收到的消息有沒有經過處理。如果已經處理過,那麼,消費者程式就不再進行處理了。

但是 List 并不會為每個消息生成 ID 号,是以我們需要自行為每個消息生成一個全局唯一ID,生成之後,我們在用 LPUSH 指令把消息插入 List 時,需要在消息中包含這個全局唯一 ID。

例如,我們執行以下指令,就把一條全局 ID 為 111000102、庫存量為 99 的消息插入了消息隊列:

> LPUSH mq "111000102:stock:99"
(integer) 1
           

3、如何保證消息可靠性?

當消費者程式從 List 中讀取一條消息後,List 就不會再留存這條消息了。是以,如果消費者程式在處理消息的過程出現了故障或當機,就會導緻消息沒有處理完成,那麼,消費者程式再次啟動後,就沒法再次從 List 中讀取消息了。

為了留存消息,List 類型提供了

BRPOPLPUSH

指令,這個指令的作用是讓消費者程式從一個 List 中讀取消息,同時,Redis 會把這個消息再插入到另一個 List(可以叫作備份 List)留存。

這樣一來,如果消費者程式讀了消息但沒能正常處理,等它重新開機後,就可以從備份 List 中重新讀取消息并進行處理了。

好了,到這裡可以知道基于 List 類型的消息隊列,滿足消息隊列的三大需求(消息保序、處理重複的消息和保證消息可靠性)。

  • 消息保序:使用 LPUSH + RPOP;
  • 阻塞讀取:使用 BRPOP;
  • 重複消息處理:生産者自行實作全局唯一 ID;
  • 消息的可靠性:使用 BRPOPLPUSH
List 作為消息隊列有什麼缺陷?

List 不支援多個消費者消費同一條消息,因為一旦消費者拉取一條消息後,這條消息就從 List 中删除了,無法被其它消費者再次消費。

要實作一條消息可以被多個消費者消費,那麼就要将多個消費者組成一個消費組,使得多個消費者可以消費同一條消息,但是 List 類型并不支援消費組的實作。

這就要說起 Redis 從 5.0 版本開始提供的 Stream 資料類型了,Stream 同樣能夠滿足消息隊列的三大需求,而且它還支援「消費組」形式的消息讀取。

Hash

介紹

Hash 是一個鍵值對(key - value)集合,其中 value 的形式入:

value=[{field1,value1},...{fieldN,valueN}]

。Hash 特别适合用于存儲對象。

Hash 與 String 對象的差別如下圖所示:

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

内部實作

Hash 類型的底層資料結構是由壓縮清單或哈希表實作的:

  • 如果哈希類型元素個數小于

    512

    個(預設值,可由

    hash-max-ziplist-entries

    配置),所有值小于

    64

    位元組(預設值,可由

    hash-max-ziplist-value

    配置)的話,Redis 會使用壓縮清單作為 Hash 類型的底層資料結構;
  • 如果哈希類型元素不滿足上面條件,Redis 會使用哈希表作為 Hash 類型的 底層資料結構。

在 Redis 7.0 中,壓縮清單資料結構已經廢棄了,交由 listpack 資料結構來實作了。

常用指令

# 存儲一個哈希表key的鍵值
HSET key field value   
# 擷取哈希表key對應的field鍵值
HGET key field

# 在一個哈希表key中存儲多個鍵值對
HMSET key field value [field value...] 
# 批量擷取哈希表key中多個field鍵值
HMGET key field [field ...]       
# 删除哈希表key中的field鍵值
HDEL key field [field ...]    

# 傳回哈希表key中field的數量
HLEN key       
# 傳回哈希表key中所有的鍵值
HGETALL key 

# 為哈希表key中field鍵的值加上增量n
HINCRBY key field n                         
           

應用場景

緩存對象

Hash 類型的 (key,field, value) 的結構與對象的(對象id, 屬性, 值)的結構相似,也可以用來存儲對象。

我們以使用者資訊為例,它在關系型資料庫中的結構是這樣的:

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

我們可以使用如下指令,将使用者對象的資訊存儲到 Hash 類型:

# 存儲一個哈希表uid:1的鍵值
> HSET uid:1 name Tom age 15
2
# 存儲一個哈希表uid:2的鍵值
> HSET uid:2 name Jerry age 13
2
# 擷取哈希表使用者id為1中所有的鍵值
> HGETALL uid:1
1) "name"
2) "Tom"
3) "age"
4) "15"
           

Redis Hash 存儲其結構如下圖:

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

在介紹 String 類型的應用場景時有所介紹,String + Json也是存儲對象的一種方式,那麼存儲對象時,到底用 String + json 還是用 Hash 呢?

一般對象用 String + Json 存儲,對象中某些頻繁變化的屬性可以考慮抽出來用 Hash 類型存儲。

購物車

以使用者 id 為 key,商品 id 為 field,商品數量為 value,恰好構成了購物車的3個要素,如下圖所示。

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

涉及的指令如下:

  • 添加商品:

    HSET cart:{使用者id} {商品id} 1

  • 添加數量:

    HINCRBY cart:{使用者id} {商品id} 1

  • 商品總數:

    HLEN cart:{使用者id}

  • 删除商品:

    HDEL cart:{使用者id} {商品id}

  • 擷取購物車所有商品:

    HGETALL cart:{使用者id}

目前僅僅是将商品ID存儲到了Redis 中,在回顯商品具體資訊的時候,還需要拿着商品 id 查詢一次資料庫,擷取完整的商品的資訊。

Set

介紹

Set 類型是一個無序并唯一的鍵值集合,它的存儲順序不會按照插入的先後順序進行存儲。

一個集合最多可以存儲

2^32-1

個元素。概念和數學中個的集合基本類似,可以交集,并集,差集等等,是以 Set 類型除了支援集合内的增删改查,同時還支援多個集合取交集、并集、差集。

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

Set 類型和 List 類型的差別如下:

  • List 可以存儲重複元素,Set 隻能存儲非重複元素;
  • List 是按照元素的先後順序存儲元素的,而 Set 則是無序方式存儲元素的。

内部實作

Set 類型的底層資料結構是由哈希表或整數集合實作的:

  • 如果集合中的元素都是整數且元素個數小于

    512

    (預設值,

    set-maxintset-entries

    配置)個,Redis 會使用整數集合作為 Set 類型的底層資料結構;
  • 如果集合中的元素不滿足上面條件,則 Redis 使用哈希表作為 Set 類型的底層資料結構。

常用指令

Set常用操作:

# 往集合key中存入元素,元素存在則忽略,若key不存在則建立
SADD key member [member ...]
# 從集合key中删除元素
SREM key member [member ...] 
# 擷取集合key中所有元素
SMEMBERS key
# 擷取集合key中的元素個數
SCARD key

# 判斷member元素是否存在于集合key中
SISMEMBER key member

# 從集合key中随機選出count個元素,元素不從key中删除
SRANDMEMBER key [count]
# 從集合key中随機選出count個元素,元素從key中删除
SPOP key [count]
           

Set運算操作:

# 交集運算
SINTER key [key ...]
# 将交集結果存入新集合destination中
SINTERSTORE destination key [key ...]

# 并集運算
SUNION key [key ...]
# 将并集結果存入新集合destination中
SUNIONSTORE destination key [key ...]

# 差集運算
SDIFF key [key ...]
# 将差集結果存入新集合destination中
SDIFFSTORE destination key [key ...]
           

應用場景

集合的主要幾個特性,無序、不可重複、支援并交差等操作。

是以 Set 類型比較适合用來資料去重和保障資料的唯一性,還可以用來統計多個集合的交集、錯集和并集等,當我們存儲的資料是無序并且需要去重的情況下,比較适合使用集合類型進行存儲。

但是要提醒你一下,這裡有一個潛在的風險。Set 的差集、并集和交集的計算複雜度較高,在資料量較大的情況下,如果直接執行這些計算,會導緻 Redis 執行個體阻塞。

在主從叢集中,為了避免主庫因為 Set 做聚合計算(交集、差集、并集)時導緻主庫被阻塞,我們可以選擇一個從庫完成聚合統計,或者把資料傳回給用戶端,由用戶端來完成聚合統計。

點贊

Set 類型可以保證一個使用者隻能點一個贊,這裡舉例子一個場景,key 是文章id,value 是使用者id。

uid:1

uid:2

uid:3

三個使用者分别對 article:1 文章點贊了。

# uid:1 使用者對文章 article:1 點贊
> SADD article:1 uid:1
(integer) 1
# uid:2 使用者對文章 article:1 點贊
> SADD article:1 uid:2
(integer) 1
# uid:3 使用者對文章 article:1 點贊
> SADD article:1 uid:3
(integer) 1
           

uid:1

取消了對 article:1 文章點贊。

> SREM article:1 uid:1
(integer) 1
           

擷取 article:1 文章所有點贊使用者 :

> SMEMBERS article:1
1) "uid:3"
2) "uid:2"
           

擷取 article:1 文章的點贊使用者數量:

> SCARD article:1
(integer) 2
           

判斷使用者

uid:1

是否對文章 article:1 點贊了:

> SISMEMBER article:1 uid:1
(integer) 0  # 傳回0說明沒點贊,傳回1則說明點贊了
           

共同關注

Set 類型支援交集運算,是以可以用來計算共同關注的好友、公衆号等。

key 可以是使用者id,value 則是已關注的公衆号的id。

uid:1

使用者關注公衆号 id 為 5、6、7、8、9,

uid:2

使用者關注公衆号 id 為 7、8、9、10、11。

# uid:1 使用者關注公衆号 id 為 5、6、7、8、9
> SADD uid:1 5 6 7 8 9
(integer) 5
# uid:2  使用者關注公衆号 id 為 7、8、9、10、11
> SADD uid:2 7 8 9 10 11
(integer) 5
           

uid:1

uid:2

共同關注的公衆号:

# 擷取共同關注
> SINTER uid:1 uid:2
1) "7"
2) "8"
3) "9"
           

uid:2

推薦

uid:1

關注的公衆号:

> SDIFF uid:1 uid:2
1) "5"
2) "6"
           

驗證某個公衆号是否同時被

uid:1

uid:2

關注:

> SISMEMBER uid:1 5
(integer) 1 # 傳回0,說明關注了
> SISMEMBER uid:2 5
(integer) 0 # 傳回0,說明沒關注
           

抽獎活動

存儲某活動中中獎的使用者名 ,Set 類型因為有去重功能,可以保證同一個使用者不會中獎兩次。

key為抽獎活動名,value為員工名稱,把所有員工名稱放入抽獎箱 :

>SADD lucky Tom Jerry John Sean Marry Lindy Sary Mark
(integer) 5
           

如果允許重複中獎,可以使用 SRANDMEMBER 指令。

# 抽取 1 個一等獎:
> SRANDMEMBER lucky 1
1) "Tom"
# 抽取 2 個二等獎:
> SRANDMEMBER lucky 2
1) "Mark"
2) "Jerry"
# 抽取 3 個三等獎:
> SRANDMEMBER lucky 3
1) "Sary"
2) "Tom"
3) "Jerry"
           

如果不允許重複中獎,可以使用 SPOP 指令。

# 抽取一等獎1個
> SPOP lucky 1
1) "Sary"
# 抽取二等獎2個
> SPOP lucky 2
1) "Jerry"
2) "Mark"
# 抽取三等獎3個
> SPOP lucky 3
1) "John"
2) "Sean"
3) "Lindy"
           

Zset

介紹

Zset 類型(有序集合類型)相比于 Set 類型多了一個排序屬性 score(分值),對于有序集合 ZSet 來說,每個存儲元素相當于有兩個值組成的,一個是有序結合的元素值,一個是排序值。

有序集合保留了集合不能有重複成員的特性(分值可以重複),但不同的是,有序集合中的元素可以排序。

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

内部實作

Zset 類型的底層資料結構是由壓縮清單或跳表實作的:

  • 如果有序集合的元素個數小于

    128

    個,并且每個元素的值小于

    64

    位元組時,Redis 會使用壓縮清單作為 Zset 類型的底層資料結構;
  • 如果有序集合的元素不滿足上面的條件,Redis 會使用跳表作為 Zset 類型的底層資料結構;

在 Redis 7.0 中,壓縮清單資料結構已經廢棄了,交由 listpack 資料結構來實作了。

常用指令

Zset 常用操作:

# 往有序集合key中加入帶分值元素
ZADD key score member [[score member]...]   
# 往有序集合key中删除元素
ZREM key member [member...]                 
# 傳回有序集合key中元素member的分值
ZSCORE key member
# 傳回有序集合key中元素個數
ZCARD key 

# 為有序集合key中元素member的分值加上increment
ZINCRBY key increment member 

# 正序擷取有序集合key從start下标到stop下标的元素
ZRANGE key start stop [WITHSCORES]
# 倒序擷取有序集合key從start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES]

# 傳回有序集合中指定分數區間内的成員,分數由低到高排序。
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

# 傳回指定成員區間内的成員,按字典正序排列, 分數必須相同。
ZRANGEBYLEX key min max [LIMIT offset count]
# 傳回指定成員區間内的成員,按字典倒序排列, 分數必須相同
ZREVRANGEBYLEX key max min [LIMIT offset count]
           

Zset 運算操作(相比于 Set 類型,ZSet 類型沒有支援差集運算):

# 并集計算(相同元素分值相加),numberkeys一共多少個key,WEIGHTS每個key對應的分值乘積
ZUNIONSTORE destkey numberkeys key [key...] 
# 交集計算(相同元素分值相加),numberkeys一共多少個key,WEIGHTS每個key對應的分值乘積
ZINTERSTORE destkey numberkeys key [key...]
           

應用場景

Zset 類型(Sorted Set,有序集合) 可以根據元素的權重來排序,我們可以自己來決定每個元素的權重值。比如說,我們可以根據元素插入 Sorted Set 的時間确定權重值,先插入的元素權重小,後插入的元素權重大。

在面對需要展示最新清單、排行榜等場景時,如果資料更新頻繁或者需要分頁顯示,可以優先考慮使用 Sorted Set。

排行榜

有序集合比較典型的使用場景就是排行榜。例如學生成績的排名榜、遊戲積分排行榜、視訊播放排名、電商系統中商品的銷量排名等。

我們以博文點贊排名為例,小林發表了五篇博文,分别獲得贊為 200、40、100、50、150。

# arcticle:1 文章獲得了200個贊
> ZADD user:xiaolin:ranking 200 arcticle:1
(integer) 1
# arcticle:2 文章獲得了40個贊
> ZADD user:xiaolin:ranking 40 arcticle:2
(integer) 1
# arcticle:3 文章獲得了100個贊
> ZADD user:xiaolin:ranking 100 arcticle:3
(integer) 1
# arcticle:4 文章獲得了50個贊
> ZADD user:xiaolin:ranking 50 arcticle:4
(integer) 1
# arcticle:5 文章獲得了150個贊
> ZADD user:xiaolin:ranking 150 arcticle:5
(integer) 1
           

文章 arcticle:4 新增一個贊,可以使用 ZINCRBY 指令(為有序集合key中元素member的分值加上increment):

> ZINCRBY user:xiaolin:ranking 1 arcticle:4
"51"
           

檢視某篇文章的贊數,可以使用 ZSCORE 指令(傳回有序集合key中元素個數):

> ZSCORE user:xiaolin:ranking arcticle:4
"50"
           

擷取小林文章贊數最多的 3 篇文章,可以使用 ZREVRANGE 指令(倒序擷取有序集合 key 從start下标到stop下标的元素):

# WITHSCORES 表示把 score 也顯示出來
> ZREVRANGE user:xiaolin:ranking 0 2 WITHSCORES
1) "arcticle:1"
2) "200"
3) "arcticle:5"
4) "150"
5) "arcticle:3"
6) "100"
           

擷取小林 100 贊到 200 贊的文章,可以使用 ZRANGEBYSCORE 指令(傳回有序集合中指定分數區間内的成員,分數由低到高排序):

> ZRANGEBYSCORE user:xiaolin:ranking 100 200 WITHSCORES
1) "arcticle:3"
2) "100"
3) "arcticle:5"
4) "150"
5) "arcticle:1"
6) "200"
           

電話、姓名排序

使用有序集合的

ZRANGEBYLEX

ZREVRANGEBYLEX

可以幫助我們實作電話号碼或姓名的排序,我們以

ZRANGEBYLEX

(傳回指定成員區間内的成員,按 key 正序排列,分數必須相同)為例。

注意:不要在分數不一緻的 SortSet 集合中去使用 ZRANGEBYLEX和 ZREVRANGEBYLEX 指令,因為擷取的結果會不準确。

1、電話排序

我們可以将電話号碼存儲到 SortSet 中,然後根據需要來擷取号段:

> ZADD phone 0 13100111100 0 13110114300 0 13132110901 
(integer) 3
> ZADD phone 0 13200111100 0 13210414300 0 13252110901 
(integer) 3
> ZADD phone 0 13300111100 0 13310414300 0 13352110901 
(integer) 3
           

擷取所有号碼:

> ZRANGEBYLEX phone - +
1) "13100111100"
2) "13110114300"
3) "13132110901"
4) "13200111100"
5) "13210414300"
6) "13252110901"
7) "13300111100"
8) "13310414300"
9) "13352110901"
           

擷取 132 号段的号碼:

> ZRANGEBYLEX phone [132 (133
1) "13200111100"
2) "13210414300"
3) "13252110901"
           

擷取132、133号段的号碼:

> ZRANGEBYLEX phone [132 (134
1) "13200111100"
2) "13210414300"
3) "13252110901"
4) "13300111100"
5) "13310414300"
6) "13352110901"
           

2、姓名排序

> zadd names 0 Toumas 0 Jake 0 Bluetuo 0 Gaodeng 0 Aimini 0 Aidehua 
(integer) 6
           

擷取所有人的名字:

> ZRANGEBYLEX names - +
1) "Aidehua"
2) "Aimini"
3) "Bluetuo"
4) "Gaodeng"
5) "Jake"
6) "Toumas"
           

擷取名字中大寫字母A開頭的所有人:

> ZRANGEBYLEX names [A (B
1) "Aidehua"
2) "Aimini"
           

擷取名字中大寫字母 C 到 Z 的所有人:

> ZRANGEBYLEX names [C [Z
1) "Gaodeng"
2) "Jake"
3) "Toumas"
           

BitMap

介紹

Bitmap,即位圖,是一串連續的二進制數組(0和1),可以通過偏移量(offset)定位元素。BitMap通過最小的機關bit來進行

0|1

的設定,表示某個元素的值或者狀态,時間複雜度為O(1)。

由于 bit 是計算機中最小的機關,使用它進行儲存将非常節省空間,特别适合一些資料量大且使用二值統計的場景。

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

内部實作

Bitmap 本身是用 String 類型作為底層資料結構實作的一種統計二值狀态的資料類型。

String 類型是會儲存為二進制的位元組數組,是以,Redis 就把位元組數組的每個 bit 位利用起來,用來表示一個元素的二值狀态,你可以把 Bitmap 看作是一個 bit 數組。

常用指令

bitmap 基本操作:

# 設定值,其中value隻能是 0 和 1
SETBIT key offset value

# 擷取值
GETBIT key offset

# 擷取指定範圍内值為 1 的個數
# start 和 end 以位元組為機關
BITCOUNT key start end
           

bitmap 運算操作:

# BitMap間的運算
# operations 位移操作符,枚舉值
  AND 與運算 &
  OR 或運算 |
  XOR 異或 ^
  NOT 取反 ~
# result 計算的結果,會存儲在該key中
# key1 … keyn 參與運算的key,可以有多個,空格分割,not運算隻能一個key
# 當 BITOP 處理不同長度的字元串時,較短的那個字元串所缺少的部分會被看作 0。傳回值是儲存到 destkey 的字元串的長度(以位元組byte為機關),和輸入 key 中最長的字元串長度相等。
BITOP [operations] [result] [key1] [keyn…]

# 傳回指定key中第一次出現指定value(0/1)的位置
BITPOS [key] [value]
           

應用場景

Bitmap 類型非常适合二值狀态統計的場景,這裡的二值狀态就是指集合元素的取值就隻有 0 和 1 兩種,在記錄海量資料時,Bitmap 能夠有效地節省記憶體空間。

簽到統計

在簽到打卡的場景中,我們隻用記錄簽到(1)或未簽到(0),是以它就是非常典型的二值狀态。

簽到統計時,每個使用者一天的簽到用 1 個 bit 位就能表示,一個月(假設是 31 天)的簽到情況用 31 個 bit 位就可以,而一年的簽到也隻需要用 365 個 bit 位,根本不用太複雜的集合類型。

假設我們要統計 ID 100 的使用者在 2022 年 6 月份的簽到情況,就可以按照下面的步驟進行操作。

第一步,執行下面的指令,記錄該使用者 6 月 3 号已簽到。

SETBIT uid:sign:100:202206 2 1
           

第二步,檢查該使用者 6 月 3 日是否簽到。

GETBIT uid:sign:100:202206 2 
           

第三步,統計該使用者在 6 月份的簽到次數。

BITCOUNT uid:sign:100:202206
           

這樣,我們就知道該使用者在 6 月份的簽到情況了。

如何統計這個月首次打卡時間呢?

Redis 提供了

BITPOS key bitValue [start] [end]

指令,傳回資料表示 Bitmap 中第一個值為

bitValue

的 offset 位置。

在預設情況下, 指令将檢測整個位圖, 使用者可以通過可選的

start

參數和

end

參數指定要檢測的範圍。是以我們可以通過執行這條指令來擷取 userID = 100 在 2022 年 6 月份首次打卡日期:

BITPOS uid:sign:100:202206 1
           

需要注意的是,因為 offset 從 0 開始的,是以我們需要将傳回的 value + 1 。

判斷使用者登陸态

Bitmap 提供了

GETBIT、SETBIT

操作,通過一個偏移值 offset 對 bit 數組的 offset 位置的 bit 位進行讀寫操作,需要注意的是 offset 從 0 開始。

隻需要一個 key = login_status 表示存儲使用者登陸狀态集合資料, 将使用者 ID 作為 offset,線上就設定為 1,下線設定 0。通過

GETBIT

判斷對應的使用者是否線上。 50000 萬 使用者隻需要 6 MB 的空間。

假如我們要判斷 ID = 10086 的使用者的登陸情況:

第一步,執行以下指令,表示使用者已登入。

SETBIT login_status 10086 1
           

第二步,檢查該使用者是否登陸,傳回值 1 表示已登入。

GETBIT login_status 10086
           

第三步,登出,将 offset 對應的 value 設定成 0。

SETBIT login_status 10086 0
           

連續簽到使用者總數

如何統計出這連續 7 天連續打卡使用者總數呢?

我們把每天的日期作為 Bitmap 的 key,userId 作為 offset,若是打卡則将 offset 位置的 bit 設定成 1。

key 對應的集合的每個 bit 位的資料則是一個使用者在該日期的打卡記錄。

一共有 7 個這樣的 Bitmap,如果我們能對這 7 個 Bitmap 的對應的 bit 位做 『與』運算。同樣的 UserID offset 都是一樣的,當一個 userID 在 7 個 Bitmap 對應對應的 offset 位置的 bit = 1 就說明該使用者 7 天連續打卡。

結果儲存到一個新 Bitmap 中,我們再通過

BITCOUNT

統計 bit = 1 的個數便得到了連續打卡 3 天的使用者總數了。

Redis 提供了

BITOP operation destkey key [key ...]

這個指令用于對一個或者多個 key 的 Bitmap 進行位元操作。

  • opration

    可以是

    and

    OR

    NOT

    XOR

    。當 BITOP 處理不同長度的字元串時,較短的那個字元串所缺少的部分會被看作 。空的

    key

    也被看作是包含 的字元串序列。

舉個例子,比如将三個 bitmap 進行 AND 操作,并将結果儲存到 destmap 中,接着對 destmap 執行 BITCOUNT 統計。

# 與操作
BITOP AND destmap bitmap:01 bitmap:02 bitmap:03
# 統計 bit 位 =  1 的個數
BITCOUNT destmap
           

即使一天産生一個億的資料,Bitmap 占用的記憶體也不大,大約占 12 MB 的記憶體(10^8/8/1024/1024),7 天的 Bitmap 的記憶體開銷約為 84 MB。同時我們最好給 Bitmap 設定過期時間,讓 Redis 删除過期的打卡資料,節省記憶體。

HyperLogLog

介紹

Redis HyperLogLog 是 Redis 2.8.9 版本新增的資料類型,是一種用于「統計基數」的資料集合類型,基數統計就是指統計一個集合中不重複的元素個數。但要注意,HyperLogLog 是統計規則是基于機率完成的,不是非常準确,标準誤算率是 0.81%。

是以,簡單來說 HyperLogLog 提供不精确的去重計數。

HyperLogLog 的優點是,在輸入元素的數量或者體積非常非常大時,計算基數所需的記憶體空間總是固定的、并且是很小的。

在 Redis 裡面,每個 HyperLogLog 鍵隻需要花費 12 KB 記憶體,就可以計算接近

2^64

個不同元素的基數,和元素越多就越耗費記憶體的 Set 和 Hash 類型相比,HyperLogLog 就非常節省空間。

這什麼概念?舉個例子給大家對比一下。

用 Java 語言來說,一般 long 類型占用 8 位元組,而 1 位元組有 8 位,即:1 byte = 8 bit,即 long 資料類型最大可以表示的數是:

2^63-1

。對應上面的

2^64

個數,假設此時有

2^63-1

這麼多個數,從

0 ~ 2^63-1

,按照

long

以及

1k = 1024 位元組

的規則來計算記憶體總數,就是:

((2^63-1) * 8/1024)K

,這是很龐大的一個數,存儲空間遠遠超過

12K

,而

HyperLogLog

卻可以用

12K

就能統計完。

内部實作

HyperLogLog 的實作涉及到很多數學問題,太費腦子了,我也沒有搞懂,如果你想了解一下,課下可以看看這個:HyperLogLog。

常見指令

HyperLogLog 指令很少,就三個。

# 添加指定元素到 HyperLogLog 中
PFADD key element [element ...]

# 傳回給定 HyperLogLog 的基數估算值。
PFCOUNT key [key ...]

# 将多個 HyperLogLog 合并為一個 HyperLogLog
PFMERGE destkey sourcekey [sourcekey ...]
           

應用場景

百萬級網頁 UV 計數

Redis HyperLogLog 優勢在于隻需要花費 12 KB 記憶體,就可以計算接近 2^64 個元素的基數,和元素越多就越耗費記憶體的 Set 和 Hash 類型相比,HyperLogLog 就非常節省空間。

是以,非常适合統計百萬級以上的網頁 UV 的場景。

在統計 UV 時,你可以用 PFADD 指令(用于向 HyperLogLog 中添加新元素)把通路頁面的每個使用者都添加到 HyperLogLog 中。

PFADD page1:uv user1 user2 user3 user4 user5
           

接下來,就可以用 PFCOUNT 指令直接獲得 page1 的 UV 值了,這個指令的作用就是傳回 HyperLogLog 的統計結果。

PFCOUNT page1:uv
           

不過,有一點需要你注意一下,HyperLogLog 的統計規則是基于機率完成的,是以它給出的統計結果是有一定誤差的,标準誤算率是 0.81%。

這也就意味着,你使用 HyperLogLog 統計的 UV 是 100 萬,但實際的 UV 可能是 101 萬。雖然誤差率不算大,但是,如果你需要精确統計結果的話,最好還是繼續用 Set 或 Hash 類型。

GEO

Redis GEO 是 Redis 3.2 版本新增的資料類型,主要用于存儲地理位置資訊,并對存儲的資訊進行操作。

在日常生活中,我們越來越依賴搜尋“附近的餐館”、在打車軟體上叫車,這些都離不開基于位置資訊服務(Location-Based Service,LBS)的應用。LBS 應用通路的資料是和人或物關聯的一組經緯度資訊,而且要能查詢相鄰的經緯度範圍,GEO 就非常适合應用在 LBS 服務的場景中。

内部實作

GEO 本身并沒有設計新的底層資料結構,而是直接使用了 Sorted Set 集合類型。

GEO 類型使用 GeoHash 編碼方法實作了經緯度到 Sorted Set 中元素權重分數的轉換,這其中的兩個關鍵機制就是「對二維地圖做區間劃分」和「對區間進行編碼」。一組經緯度落在某個區間後,就用區間的編碼值來表示,并把編碼值作為 Sorted Set 元素的權重分數。

這樣一來,我們就可以把經緯度儲存到 Sorted Set 中,利用 Sorted Set 提供的“按權重進行有序範圍查找”的特性,實作 LBS 服務中頻繁使用的“搜尋附近”的需求。

常用指令

# 存儲指定的地理空間位置,可以将一個或多個經度(longitude)、緯度(latitude)、位置名稱(member)添加到指定的 key 中。
GEOADD key longitude latitude member [longitude latitude member ...]

# 從給定的 key 裡傳回所有指定名稱(member)的位置(經度和緯度),不存在的傳回 nil。
GEOPOS key member [member ...]

# 傳回兩個給定位置之間的距離。
GEODIST key member1 member2 [m|km|ft|mi]

# 根據使用者給定的經緯度坐标來擷取指定範圍内的地理位置集合。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
           

應用場景

滴滴叫車

這裡以滴滴叫車的場景為例,介紹下具體如何使用 GEO 指令:GEOADD 和 GEORADIUS 這兩個指令。

假設車輛 ID 是 33,經緯度位置是(116.034579,39.030452),我們可以用一個 GEO 集合儲存所有車輛的經緯度,集合 key 是 cars:locations。

執行下面的這個指令,就可以把 ID 号為 33 的車輛的目前經緯度位置存入 GEO 集合中:

GEOADD cars:locations 116.034579 39.030452 33
           

當使用者想要尋找自己附近的網約車時,LBS 應用就可以使用 GEORADIUS 指令。

例如,LBS 應用執行下面的指令時,Redis 會根據輸入的使用者的經緯度資訊(116.054579,39.030452 ),查找以這個經緯度為中心的 5 公裡内的車輛資訊,并傳回給 LBS 應用。

GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10
           

Stream

介紹

Redis Stream 是 Redis 5.0 版本新增加的資料類型,Redis 專門為消息隊列設計的資料類型。

在 Redis 5.0 Stream 沒出來之前,消息隊列的實作方式都有着各自的缺陷,例如:

  • 釋出訂閱模式,不能持久化也就無法可靠的儲存消息,并且對于離線重連的用戶端不能讀取曆史消息的缺陷;
  • List 實作消息隊列的方式不能重複消費,一個消息消費完就會被删除,而且生産者需要自行實作全局唯一 ID。

基于以上問題,Redis 5.0 便推出了 Stream 類型也是此版本最重要的功能,用于完美地實作消息隊列,它支援消息的持久化、支援自動生成全局唯一 ID、支援 ack 确認消息的模式、支援消費組模式等,讓消息隊列更加的穩定和可靠。

常見指令

Stream 消息隊列操作指令:

  • XADD:插入消息,保證有序,可以自動生成全局唯一 ID;
  • XLEN :查詢消息長度;
  • XREAD:用于讀取消息,可以按 ID 讀取資料;
  • XDEL : 根據消息 ID 删除消息;
  • DEL :删除整個 Stream;
  • XRANGE :讀取區間消息
  • XREADGROUP:按消費組形式讀取消息;
  • XPENDING 和 XACK:
    • XPENDING 指令可以用來查詢每個消費組内所有消費者「已讀取、但尚未确認」的消息;
    • XACK 指令用于向消息隊列确認消息處理已完成;

應用場景

消息隊列

生産者通過 XADD 指令插入一條消息:

# * 表示讓 Redis 為插入的資料自動生成一個全局唯一的 ID
# 往名稱為 mymq 的消息隊列中插入一條消息,消息的鍵是 name,值是 xiaolin
> XADD mymq * name xiaolin
"1654254953808-0"
           

插入成功後會傳回全局唯一的 ID:"1654254953808-0"。消息的全局唯一 ID 由兩部分組成:

  • 第一部分“1654254953808”是資料插入時,以毫秒為機關計算的目前伺服器時間;
  • 第二部分表示插入消息在目前毫秒内的消息序号,這是從 0 開始編号的。例如,“1654254953808-0”就表示在“1654254953808”毫秒内的第 1 條消息。

消費者通過 XREAD 指令從消息隊列中讀取消息時,可以指定一個消息 ID,并從這個消息 ID 的下一條消息開始進行讀取(注意是輸入消息 ID 的下一條資訊開始讀取,不是查詢輸入ID的消息)。

# 從 ID 号為 1654254953807-0 的消息開始,讀取後續的所有消息(示例中一共 1 條)。
> XREAD STREAMS mymq 1654254953807-0
1) 1) "mymq"
   2) 1) 1) "1654254953808-0"
         2) 1) "name"
            2) "xiaolin"
           

如果想要實作阻塞讀(當沒有資料時,阻塞住),可以調用 XRAED 時設定 BLOCK 配置項,實作類似于 BRPOP 的阻塞讀取操作。

比如,下面這指令,設定了 BLOCK 10000 的配置項,10000 的機關是毫秒,表明 XREAD 在讀取最新消息時,如果沒有消息到來,XREAD 将阻塞 10000 毫秒(即 10 秒),然後再傳回。

# 指令最後的“$”符号表示讀取最新的消息
> XREAD BLOCK 10000 STREAMS mymq $
(nil)
(10.00s)
           

Stream 的基礎方法,使用 xadd 存入消息和 xread 循環阻塞讀取消息的方式可以實作簡易版的消息隊列,互動流程如下圖所示:

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景
前面介紹的這些操作 List 也支援的,接下來看看 Stream 特有的功能。

Stream 可以以使用 XGROUP 建立消費組,建立消費組之後,Stream 可以使用 XREADGROUP 指令讓消費組内的消費者讀取消息。

建立兩個消費組,這兩個消費組消費的消息隊列是 mymq,都指定從第一條消息開始讀取:

# 建立一個名為 group1 的消費組,0-0 表示從第一條消息開始讀取。
> XGROUP CREATE mymq group1 0-0
OK
# 建立一個名為 group2 的消費組,0-0 表示從第一條消息開始讀取。
> XGROUP CREATE mymq group2 0-0
OK
           

消費組 group1 内的消費者 consumer1 從 mymq 消息隊列中讀取所有消息的指令如下:

# 指令最後的參數“>”,表示從第一條尚未被消費的消息開始讀取。
> XREADGROUP GROUP group1 consumer1 STREAMS mymq >
1) 1) "mymq"
   2) 1) 1) "1654254953808-0"
         2) 1) "name"
            2) "xiaolin"
           

消息隊列中的消息一旦被消費組裡的一個消費者讀取了,就不能再被該消費組内的其他消費者讀取了,即同一個消費組裡的消費者不能消費同一條消息。

比如說,我們執行完剛才的 XREADGROUP 指令後,再執行一次同樣的指令,此時讀到的就是空值了:

> XREADGROUP GROUP group1 consumer1 STREAMS mymq >
(nil)
           

但是,不同消費組的消費者可以消費同一條消息(但是有前提條件,建立消息組的時候,不同消費組指定了相同位置開始讀取消息)。

比如說,剛才 group1 消費組裡的 consumer1 消費者消費了一條 id 為 1654254953808-0 的消息,現在用 group2 消費組裡的 consumer1 消費者消費消息:

> XREADGROUP GROUP group2 consumer1 STREAMS mymq >
1) 1) "mymq"
   2) 1) 1) "1654254953808-0"
         2) 1) "name"
            2) "xiaolin"
           

因為我建立兩組的消費組都是從第一條消息開始讀取,是以可以看到第二組的消費者依然可以消費 id 為 1654254953808-0 的這一條消息。是以,不同的消費組的消費者可以消費同一條消息。

使用消費組的目的是讓組内的多個消費者共同分擔讀取消息,是以,我們通常會讓每個消費者讀取部分消息,進而實作消息讀取負載在多個消費者間是均衡分布的。

例如,我們執行下列指令,讓 group2 中的 consumer1、2、3 各自讀取一條消息。

# 讓 group2 中的 consumer1 從 mymq 消息隊列中消費一條消息
> XREADGROUP GROUP group2 consumer1 COUNT 1 STREAMS mymq >
1) 1) "mymq"
   2) 1) 1) "1654254953808-0"
         2) 1) "name"
            2) "xiaolin"
# 讓 group2 中的 consumer2 從 mymq 消息隊列中消費一條消息
> XREADGROUP GROUP group2 consumer2 COUNT 1 STREAMS mymq >
1) 1) "mymq"
   2) 1) 1) "1654256265584-0"
         2) 1) "name"
            2) "xiaolincoding"
# 讓 group2 中的 consumer3 從 mymq 消息隊列中消費一條消息
> XREADGROUP GROUP group2 consumer3 COUNT 1 STREAMS mymq >
1) 1) "mymq"
   2) 1) 1) "1654256271337-0"
         2) 1) "name"
            2) "Tom"
           
基于 Stream 實作的消息隊列,如何保證消費者在發生故障或當機再次重新開機後,仍然可以讀取未處理完的消息?

Streams 會自動使用内部隊列(也稱為 PENDING List)留存消費組裡每個消費者讀取的消息,直到消費者使用 XACK 指令通知 Streams“消息已經處理完成”。

消費确認增加了消息的可靠性,一般在業務處理完成之後,需要執行 XACK 指令确認消息已經被消費完成,整個流程的執行如下圖所示:

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

如果消費者沒有成功處理消息,它就不會給 Streams 發送 XACK 指令,消息仍然會留存。此時,消費者可以在重新開機後,用 XPENDING 指令檢視已讀取、但尚未确認處理完成的消息。

例如,我們來檢視一下 group2 中各個消費者已讀取、但尚未确認的消息個數,指令如下:

127.0.0.1:6379> XPENDING mymq group2
1) (integer) 3
2) "1654254953808-0"  # 表示 group2 中所有消費者讀取的消息最小 ID
3) "1654256271337-0"  # 表示 group2 中所有消費者讀取的消息最大 ID
4) 1) 1) "consumer1"
      2) "1"
   2) 1) "consumer2"
      2) "1"
   3) 1) "consumer3"
      2) "1"
           

如果想檢視某個消費者具體讀取了哪些資料,可以執行下面的指令:

# 檢視 group2 裡 consumer2 已從 mymq 消息隊列中讀取了哪些消息
> XPENDING mymq group2 - + 10 consumer2
1) 1) "1654256265584-0"
   2) "consumer2"
   3) (integer) 410700
   4) (integer) 1
           

可以看到,consumer2 已讀取的消息的 ID 是 1654256265584-0。

一旦消息 1654256265584-0 被 consumer2 處理了,consumer2 就可以使用 XACK 指令通知 Streams,然後這條消息就會被删除。

> XACK mymq group2 1654256265584-0
(integer) 1
           

當我們再使用 XPENDING 指令檢視時,就可以看到,consumer2 已經沒有已讀取、但尚未确認處理的消息了。

> XPENDING mymq group2 - + 10 consumer2
(empty array)
           

好了,基于 Stream 實作的消息隊列就說到這裡了,小結一下:

  • 消息保序:XADD/XREAD
  • 阻塞讀取:XREAD block
  • 重複消息處理:Stream 在使用 XADD 指令,會自動生成全局唯一 ID;
  • 消息可靠性:内部使用 PENDING List 自動儲存消息,使用 XPENDING 指令檢視消費組已經讀取但是未被确認的消息,消費者使用 XACK 确認消息;
  • 支援消費組形式消費資料
Redis 基于 Stream 消息隊列與專業的消息隊列有哪些差距?

一個專業的消息隊列,必須要做到兩大塊:

  • 消息不丢。
  • 消息可堆積。

1、Redis Stream 消息會丢失嗎?

使用一個消息隊列,其實就分為三大塊:生産者、隊列中間件、消費者,是以要保證消息就是保證三個環節都不能丢失資料。

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

Redis Stream 消息隊列能不能保證三個環節都不丢失資料?

  • Redis 生産者會不會丢消息?生産者會不會丢消息,取決于生産者對于異常情況的處理是否合理。 從消息被生産出來,然後送出給 MQ 的過程中,隻要能正常收到 ( MQ 中間件) 的 ack 确認響應,就表示發送成功,是以隻要處理好傳回值和異常,如果傳回異常則進行消息重發,那麼這個階段是不會出現消息丢失的。
  • Redis 消費者會不會丢消息?不會,因為 Stream ( MQ 中間件)會自動使用内部隊列(也稱為 PENDING List)留存消費組裡每個消費者讀取的消息,但是未被确認的消息。消費者可以在重新開機後,用 XPENDING 指令檢視已讀取、但尚未确認處理完成的消息。等到消費者執行完業務邏輯後,再發送消費确認 XACK 指令,也能保證消息的不丢失。
  • Redis 消息中間件會不會丢消息?會,Redis 在以下 2 個場景下,都會導緻資料丢失:
    • AOF 持久化配置為每秒寫盤,但這個寫盤過程是異步的,Redis 當機時會存在資料丢失的可能
    • 主從複制也是異步的,主從切換時,也存在丢失資料的可能。

可以看到,Redis 在隊列中間件環節無法保證消息不丢。像 RabbitMQ 或 Kafka 這類專業的隊列中間件,在使用時是部署一個叢集,生産者在釋出消息時,隊列中間件通常會寫「多個節點」,也就是有多個副本,這樣一來,即便其中一個節點挂了,也能保證叢集的資料不丢失。

2、Redis Stream 消息可堆積嗎?

Redis 的資料都存儲在記憶體中,這就意味着一旦發生消息積壓,則會導緻 Redis 的記憶體持續增長,如果超過機器記憶體上限,就會面臨被 OOM 的風險。

是以 Redis 的 Stream 提供了可以指定隊列最大長度的功能,就是為了避免這種情況發生。

當指定隊列最大長度時,隊列長度超過上限後,舊消息會被删除,隻保留固定長度的新消息。這麼來看,Stream 在消息積壓時,如果指定了最大長度,還是有可能丢失消息的。

但 Kafka、RabbitMQ 專業的消息隊列它們的資料都是存儲在磁盤上,當消息積壓時,無非就是多占用一些磁盤空間。

是以,把 Redis 當作隊列來使用時,會面臨的 2 個問題:

  • Redis 本身可能會丢資料;
  • 面對消息擠壓,記憶體資源會緊張;

是以,能不能将 Redis 作為消息隊列來使用,關鍵看你的業務場景:

  • 如果你的業務場景足夠簡單,對于資料丢失不敏感,而且消息積壓機率比較小的情況下,把 Redis 當作隊列是完全可以的。
  • 如果你的業務有海量消息,消息積壓的機率比較大,并且不能接受資料丢失,那麼還是用專業的消息隊列中間件吧。
補充:Redis 釋出/訂閱機制為什麼不可以作為消息隊列?

釋出訂閱機制存在以下缺點,都是跟丢失資料有關:

  1. 釋出/訂閱機制沒有基于任何資料類型實作,是以不具備「資料持久化」的能力,也就是釋出/訂閱機制的相關操作,不會寫入到 RDB 和 AOF 中,當 Redis 當機重新開機,釋出/訂閱機制的資料也會全部丢失。
  2. 釋出訂閱模式是“發後既忘”的工作模式,如果有訂閱者離線重連之後不能消費之前的曆史消息。
  3. 當消費端有一定的消息積壓時,也就是生産者發送的消息,消費者消費不過來時,如果超過 32M 或者是 60s 内持續保持在 8M 以上,消費端會被強行斷開,這個參數是在配置檔案中設定的,預設值是

    client-output-buffer-limit pubsub 32mb 8mb 60

是以,釋出/訂閱機制隻适合即使通訊的場景,比如建構哨兵叢集的場景采用了釋出/訂閱機制。

總結

Redis 常見的五種資料類型:**String(字元串),Hash(哈希),List(清單),Set(集合)及 Zset(sorted set:有序集合)**。

這五種資料類型都由多種資料結構實作的,主要是出于時間和空間的考慮,當資料量小的時候使用更簡單的資料結構,有利于節省記憶體,提高性能。

這五種資料類型與底層資料結構對應關系圖如下,左邊是 Redis 3.0版本的,也就是《Redis 設計與實作》這本書講解的版本,現在看還是有點過時了,右邊是現在 Github 最新的 Redis 代碼的。

2 萬字 + 20張圖| 細說 Redis 九種資料類型和應用場景

可以看到,Redis 資料類型的底層資料結構随着版本的更新也有所不同,比如:

  • 在 Redis 3.0 版本中 List 對象的底層資料結構由「雙向連結清單」或「壓縮表清單」實作,但是在 3.2 版本之後,List 資料類型底層資料結構是由 quicklist 實作的;
  • 在最新的 Redis 代碼中,壓縮清單資料結構已經廢棄了,交由 listpack 資料結構來實作了。

Redis 五種資料類型的應用場景:

  • String 類型的應用場景:緩存對象、正常計數、分布式鎖、共享session資訊等。
  • List 類型的應用場景:消息隊列(有兩個問題:1. 生産者需要自行實作全局唯一 ID;2. 不能以消費組形式消費資料)等。
  • Hash 類型:緩存對象、購物車等。
  • Set 類型:聚合計算(并集、交集、差集)場景,比如點贊、共同關注、抽獎活動等。
  • Zset 類型:排序場景,比如排行榜、電話和姓名排序等。

Redis 後續版本又支援四種資料類型,它們的應用場景如下:

  • BitMap(2.2 版新增):二值狀态統計的場景,比如簽到、判斷使用者登陸狀态、連續簽到使用者總數等;
  • HyperLogLog(2.8 版新增):海量資料基數統計的場景,比如百萬級網頁 UV 計數等;
  • GEO(3.2 版新增):存儲地理位置資訊的場景,比如滴滴叫車;
  • Stream(5.0 版新增):消息隊列,相比于基于 List 類型實作的消息隊列,有這兩個特有的特性:自動生成全局唯一消息ID,支援以消費組形式消費資料。

針對 Redis 是否适合做消息隊列,關鍵看你的業務場景:

  • 如果你的業務場景足夠簡單,對于資料丢失不敏感,而且消息積壓機率比較小的情況下,把 Redis 當作隊列是完全可以的。
  • 如果你的業務有海量消息,消息積壓的機率比較大,并且不能接受資料丢失,那麼還是用專業的消息隊列中間件吧。

參考資料:

  • 《Redis 核心技術與實戰》
  • https://www.cnblogs.com/hunternet/p/12742390.html
  • https://www.cnblogs.com/qdhxhz/p/15669348.html
  • https://www.cnblogs.com/bbgs-xc/p/14376109.html
  • http://kaito-kidd.com/2021/04/19/can-redis-be-used-as-a-queue/
  • 資料類型篇
    • Redis 資料類型和應用場景
    • 圖解 Redis 資料結構
  • 持久化篇
    • AOF 持久化是怎麼實作的?
    • RDB 快照是怎麼實作的?
  • 叢集篇
    • 什麼是緩存雪崩、擊穿、穿透?
    • 主從複制是怎麼實作的?
    • 為什麼要有哨兵?
  • 架構篇
    • 資料庫和緩存如何保證一緻性?

微信搜尋公衆号:「小林coding」 ,回複「圖解」即可免費獲得「圖解網絡、圖解系統、圖解MySQL、圖解Redis」PDF 電子書