Day 6 拿 List 當 Queue,最後列了三個缺點:沒有 ACK、沒有重試、沒有消費者群組。今天的 Stream 三個都有,一個一個來看。
Stream 是一份只會往後長的訊息紀錄,每一筆有自己的 ID,後面跟著任意多組 field-value。
XADD key * field value ...(加一筆,* 是讓 Redis 自己產 ID,回傳的就是那筆的 ID)
XLEN key(有幾筆)
XRANGE key - +(撈一段出來,- 到 + 就是全部)
XGROUP CREATE key group 0(建消費者群組,0 是從第一筆開始發)
XREADGROUP GROUP group consumer COUNT n STREAMS key >(用某個消費者的身分讀,> 是「還沒發給任何人的」)
XACK key group id ...(回報處理完了,回傳成功幾筆)
XPENDING key group(誰借走了還沒還)| 先建一個消費者 worker-2 拿走一條消息
XAUTOCLAIM key group consumer min-idle-time 0(把借太久沒還的搶過來)
下單就往 stream 丟一筆,誰要用誰自己來讀:
XADD order:stream * orderId 1 amount 100
XADD order:stream * orderId 2 amount 250
XADD order:stream * orderId 3 amount 80
XLEN order:stream # 3
XRANGE order:stream - +

XADD 回傳的格式是「毫秒時間戳-序號」。同一毫秒進來好幾筆就往後加序號,所以不會重複,而且一定照時間排。
跟 List 最大的差別在這裡:RPOP 拿走就沒了,Stream 讀完資料還留在裡面。
訂單多的時候,同一支程式會開好幾份一起跑。但每一份都讀同一個 stream 的話,同一筆訂單就會被扣好幾次款。
放進同一個群組,Redis 就保證一筆只發給其中一個。g1 是群組名,c1、c2 是兩份程式:
XGROUP CREATE order:stream g1 0
XREADGROUP GROUP g1 c1 COUNT 2 STREAMS order:stream > # c1 拿到前兩筆
XREADGROUP GROUP g1 c2 COUNT 2 STREAMS order:stream > # c2 只拿到第三筆
XREADGROUP GROUP g1 c1 COUNT 2 STREAMS order:stream > # 沒了,回 nil

XREADGROUP 拿到只是「借走」,不算處理完:
XPENDING order:stream g1 # 3 筆,c1 兩筆、c2 一筆
XACK order:stream g1 1790047274061-0 1790047277791-0 # c1 處理完了,回傳 2
XPENDING order:stream g1 # 剩 1 筆,c2 那筆還掛著
沒 XACK 之前,這筆會一直掛在 pending 清單上。List 拿出去就消失了,機器處理到一半掛掉,沒人知道那筆訂單存在過;Stream 記得「這筆被誰借走、借多久了」。
XACK 只是把訊息從 pending 清單拿掉,不會刪資料。 三筆全部 ACK 完,XLEN 還是 3。
XPENDING 加上範圍參數就看得到細節:
XPENDING order:stream g1 - + 10 # 1790047280883-0 c2 228660 1

四個欄位是:訊息 ID、被誰借走、借了幾毫秒沒還、總共被發過幾次。
c2 掛掉了,那筆訂單就卡在這。XAUTOCLAIM 把借太久的搶過來給別人做:
XAUTOCLAIM order:stream g1 c1 10000 0 # 門檻 10 秒
XPENDING order:stream g1 - + 10 # 1790047280883-0 c1 5723 2

10000 是「借超過幾毫秒才能搶」,最後那個 0 是從哪個 ID 開始掃。搶成功之後借的人從 c2 變成 c1,被發過的次數從 1 變 2,這就是重試。
次數這個欄位要記得看。同一筆一直被搶來搶去,代表它根本處理不了(髒資料、外部服務掛了),次數太多就該挑出來人工處理,不然它會永遠在群組裡繞。
Stream 不會自己清。 ACK 不刪資料,讀過也不刪,就一路往後長。要限制長度得自己在 XADD 帶 MAXLEN:
XADD order:stream MAXLEN ~ 10 * orderId 1 amount 100
但 ~ 有陷阱。連續 XADD 300 筆、每筆都帶 MAXLEN ~ 10,最後量出來是 100 筆,不是 10(這行在主機的終端機下,不是在 redis-cli 裡面):
for i in $(seq 1 300); do docker exec redis30days redis-cli XADD trim:demo MAXLEN '~' 10 '*' n $i > /dev/null; done
XLEN trim:demo # 100

~ 的意思是「至少留 10 筆」,Redis 刪的時候是整個節點整個節點刪(stream-node-max-entries 預設 100),刪起來不划算就不刪。要精確就拿掉 ~ 寫 MAXLEN 10,同樣塞 300 筆就真的只剩 10 筆,代價是每次 XADD 都得真的去砍一次。
Spring Data Redis 3.4.1 沒有包 XAUTOCLAIM,只有 XCLAIM。Java 這邊得先 XPENDING 撈出來、自己比 idle 時間,再把超時的幾筆 XCLAIM 過來,Day 7 的 SINTERCARD、Day 9 的 BITCOUNT 也是一樣。
另外 Stream 有 ACK、有重試、有消費者群組,但沒有死信佇列、沒有路由、沒有延遲投遞。 跟 Day 6 的結論一樣,真的要當主要 MQ 還是得去用 RabbitMQ 或 Kafka。
今天內容比較多,Java 範例就先不列出來了~
Java 範例程式碼可以參考 https://github.com/gary880306/redis-30days/tree/dev
七天都在講「哪個型別解決哪個問題」,但同樣存十個欄位,記憶體有時候省有時候不省,差在 Redis 偷偷換了底層結構。明天用 OBJECT ENCODING 看它什麼時候換,還有換過去為什麼回不來![]()