Stream 類型
Lettuce Java 客戶端: http://tgrall.github.io/blog/2019/09/02/getting-with-redis-streams-and-java/
Jedis JAVA 客戶端:https://blog.csdn.net/weixin_37703281/article/details/93463032
package com.gdut.redisdemo.controller;
import ch.qos.logback.core.util.TimeUtil;
import com.sun.org.apache.xpath.internal.operations.Bool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.StreamEntry;
import redis.clients.jedis.StreamEntryID;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @author lulu
* @Date 2019/6/24 17:02
*/
@RestController
public class RedisController {
private static int i = 0;
@Autowired
private Jedis jedis;
@GetMapping("/createComsumer")
public void createCrgoup(@RequestParam("stream") String stream, @RequestParam("group") String group
, @RequestParam("make") Boolean b
) {
//String key, String groupname, StreamEntryID id, boolean makeStream
/**
* key爲stream name, group爲消費組,id爲上次讀取的位置,如果空則重新讀取,makeStream是否創建流,已有的話就不用創建
*/
System.out.println(jedis.xgroupCreate(stream, group, null, b));
}
@GetMapping("/add")
public void addMessage(@RequestParam("stream") String stream) {
//這裏可以添加更多的屬性
Map map = new HashMap();
map.put("date", System.currentTimeMillis() + "");
jedis.xadd(stream, new StreamEntryID(map.get("date") + "-" + (++i)), map);
}
@GetMapping("/read")
public void readGroup(@RequestParam("group") String group,
@RequestParam("consumer") String consumer,
@RequestParam("count") int count,
@RequestParam("stream") String stream
) {
Map<String,StreamEntryID> t = new HashMap();
t.put(stream, null);//null 則爲 > 重頭讀起,也可以爲$接受新消息,還可以是上一次未讀完的消息id
Map.Entry e = null;
for(Map.Entry c:t.entrySet()){
e=c;
}
//noAck爲false的話需要手動ack,true則自動ack. commsumer新建的方式爲xreadgroup。
List<Map.Entry<String, StreamEntryID>> list = jedis.xreadGroup(group, consumer, count, 0, false, e);
for (Map.Entry m : list) {
System.out.println(m.getKey() + "---" + m.getValue().getClass());
if (m.getValue() instanceof ArrayList) {
List<StreamEntry> l = (List) m.getValue();
Map<String, String> result = l.get(0).getFields();
for (Map.Entry entry : result.entrySet()) {
System.out.println(entry.getKey() + "---" + entry.getValue());
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
jedis.xack(stream, group, l.get(0).getID());
System.out.println("消息消費成功");
}
}
}
/**
* String groupname, String consumer, int count, long block, final boolean noAck, Map.Entry<String, StreamEntryID>... streams
*/
}
轉載:http://www.hellokang.net/redis/stream.html#_10-%E5%91%BD%E4%BB%A4%E4%B8%80%E8%A7%88
目錄
#1 概述
Redis5.0帶來了Stream類型。從字面上看是流類型,但其實從功能上看,應該是Redis對消息隊列(MQ,Message Queue)的完善實現。用過Redis做消息隊列的都瞭解,基於Reids的消息隊列實現有很多種,例如:
- PUB/SUB,訂閱/發佈模式
- 基於List的 LPUSH+BRPOP 的實現
- 基於Sorted-Set的實現
每一種實現,都有典型的特點和問題,這個在 Redis 實現消息隊列一文中有介紹。基於Redis實現消息隊列http://www.hellokang.net/redis/message-queue-by-redis.html
Redis5.0中發佈的Stream類型,也用來實現典型的消息隊列。該Stream類型的出現,幾乎滿足了消息隊列具備的全部內容,包括但不限於:
- 消息ID的序列化生成
- 消息遍歷
- 消息的阻塞和非阻塞讀取
- 消息的分組消費
- 未完成消息的處理
- 消息隊列監控
消息隊列有生產消息者和消費消息者,下面就體驗一下Stream類型的精彩:
#2 追加新消息,XADD,生產消息
XADD,命令用於在某個stream(流數據)中追加消息,演示如下:
127.0.0.1:6379> XADD memberMessage * user kang msg Hello
"1553439850328-0"
127.0.0.1:6379> XADD memberMessage * user zhong msg nihao
"1553439858868-0"
其中語法格式爲:
XADD key ID field string [field string ...]
需要提供key,消息ID方案,消息內容,其中消息內容爲key-value型數據。 ID,最常使用*,表示由Redis生成消息ID,這也是強烈建議的方案。 field string [field string], 就是當前消息內容,由1個或多個key-value構成。
上面的例子中,在memberMemsages這個key中追加了user kang msg Hello
這個消息。Redis使用毫秒時間戳和序號生成了消息ID。此時,消息隊列中就有一個消息可用了。
#3 從消息隊列中獲取消息,XREAD,消費消息
XREAD,從Stream中讀取消息,演示如下:
127.0.0.1:6379> XREAD streams memberMessage 0
1) 1) "memberMessage"
2) 1) 1) "1553439850328-0"
2) 1) "user"
2) "kang"
3) "msg"
4) "Hello"
2) 1) "1553439858868-0"
2) 1) "user"
2) "zhong"
3) "msg"
4) "nihao"
上面的命令是從消息隊列memberMessage中讀取所有消息。XREAD支持很多參數,語法格式爲:
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]
其中:
- [COUNT count],用於限定獲取的消息數量
- [BLOCK milliseconds],用於設置XREAD爲阻塞模式,默認爲非阻塞模式
- ID,用於設置由哪個消息ID開始讀取。使用0表示從第一條消息開始。(本例中就是使用0)此處需要注意,消息隊列ID是單調遞增的,所以通過設置起點,可以向後讀取。在阻塞模式中,可以使用$,表示最新的消息ID。(在非阻塞模式下$無意義)。
XRED讀消息時分爲阻塞和非阻塞模式,使用BLOCK選項可以表示阻塞模式,需要設置阻塞時長。非阻塞模式下,讀取完畢(即使沒有任何消息)立即返回,而在阻塞模式下,若讀取不到內容,則阻塞等待。
一個典型的阻塞模式用法爲:
127.0.0.1:6379> XREAD block 1000 streams memberMessage $
(nil)
(1.07s)
我們使用Block模式,配合$作爲ID,表示讀取最新的消息,若沒有消息,命令阻塞!等待過程中,其他客戶端向隊列追加消息,則會立即讀取到。
因此,典型的隊列就是 XADD 配合 XREAD Block 完成。XADD負責生成消息,XREAD負責消費消息。
#4 消息ID說明
XADD生成的1553439850328-0
,就是Redis生成的消息ID,由兩部分組成:時間戳-序號。時間戳是毫秒級單位,是生成消息的Redis服務器時間,它是個64位整型(int64)。序號是在這個毫秒時間點內的消息序號,它也是個64位整型。較真來說,序號可能會溢出,but真可能嗎?
可以通過multi批處理,來驗證序號的遞增:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> XADD memberMessage * msg one
QUEUED
127.0.0.1:6379> XADD memberMessage * msg two
QUEUED
127.0.0.1:6379> XADD memberMessage * msg three
QUEUED
127.0.0.1:6379> XADD memberMessage * msg four
QUEUED
127.0.0.1:6379> XADD memberMessage * msg five
QUEUED
127.0.0.1:6379> EXEC
1) "1553441006884-0"
2) "1553441006884-1"
3) "1553441006884-2"
4) "1553441006884-3"
5) "1553441006884-4"
由於一個redis命令的執行很快,所以可以看到在同一時間戳內,是通過序號遞增來表示消息的。
爲了保證消息是有序的,因此Redis生成的ID是單調遞增有序的。由於ID中包含時間戳部分,爲了避免服務器時間錯誤而帶來的問題(例如服務器時間延後了),Redis的每個Stream類型數據都維護一個latest_generated_id屬性,用於記錄最後一個消息的ID。若發現當前時間戳退後(小於latest_generated_id所記錄的),則採用時間戳不變而序號遞增的方案來作爲新消息ID(這也是序號爲什麼使用int64的原因,保證有足夠多的的序號),從而保證ID的單調遞增性質。
強烈建議使用Redis的方案生成消息ID,因爲這種時間戳+序號的單調遞增的ID方案,幾乎可以滿足你全部的需求。但同時,記住ID是支持自定義的,別忘了!
#5 消費者組模式,consumer group
當多個消費者(consumer)同時消費一個消息隊列時,可以重複的消費相同的消息,就是消息隊列中有10條消息,三個消費者都可以消費到這10條消息。
但有時,我們需要多個消費者配合協作來消費同一個消息隊列,就是消息隊列中有10條消息,三個消費者分別消費其中的某些消息,比如消費者A消費消息1、2、5、8,消費者B消費消息4、9、10,而消費者C消費消息3、6、7。也就是三個消費者配合完成消息的消費,可以在消費能力不足,也就是消息處理程序效率不高時,使用該模式。該模式就是消費者組模式。如下圖所示:
消費者組模式的支持主要由兩個命令實現:
- XGROUP,用於管理消費者組,提供創建組,銷燬組,更新組起始消息ID等操作
- XREADGROUP,分組消費消息操作
進行演示,演示時使用5個消息,思路是:創建一個Stream消息隊列,生產者生成5條消息。在消息隊列上創建一個消費組,組內三個消費者進行消息消費:
# 生產者生成10條消息
127.0.0.1:6379> MULTI
127.0.0.1:6379> XADD mq * msg 1 # 生成一個消息:msg 1
127.0.0.1:6379> XADD mq * msg 2
127.0.0.1:6379> XADD mq * msg 3
127.0.0.1:6379> XADD mq * msg 4
127.0.0.1:6379> XADD mq * msg 5
127.0.0.1:6379> EXEC
1) "1553585533795-0"
2) "1553585533795-1"
3) "1553585533795-2"
4) "1553585533795-3"
5) "1553585533795-4"
# 創建消費組 mqGroup
127.0.0.1:6379> XGROUP CREATE mq mqGroup 0 # 爲消息隊列 mq 創建消費組 mgGroup
OK
# 消費者A,消費第1條
127.0.0.1:6379> XREADGROUP group mqGroup consumerA count 1 streams mq > #消費組內消費者A,從消息隊列mq中讀取一個消息
1) 1) "mq"
2) 1) 1) "1553585533795-0"
2) 1) "msg"
2) "1"
# 消費者A,消費第2條
127.0.0.1:6379> XREADGROUP GROUP mqGroup consumerA COUNT 1 STREAMS mq >
1) 1) "mq"
2) 1) 1) "1553585533795-1"
2) 1) "msg"
2) "2"
# 消費者B,消費第3條
127.0.0.1:6379> XREADGROUP GROUP mqGroup consumerB COUNT 1 STREAMS mq >
1) 1) "mq"
2) 1) 1) "1553585533795-2"
2) 1) "msg"
2) "3"
# 消費者A,消費第4條
127.0.0.1:6379> XREADGROUP GROUP mqGroup consumerA count 1 STREAMS mq >
1) 1) "mq"
2) 1) 1) "1553585533795-3"
2) 1) "msg"
2) "4"
# 消費者C,消費第5條
127.0.0.1:6379> XREADGROUP GROUP mqGroup consumerC COUNT 1 STREAMS mq >
1) 1) "mq"
2) 1) 1) "1553585533795-4"
2) 1) "msg"
2) "5"
上面的例子中,三個在同一組 mpGroup 消費者A、B、C在消費消息時(消費者在消費時指定即可,不用預先創建),有着互斥原則,消費方案爲,A->1, A->2, B->3, A->4, C->5。語法說明爲:
XGROUP CREATE mq mqGroup 0
,用於在消息隊列mq上創建消費組 mpGroup,最後一個參數0,表示該組從第一條消息開始消費。(意義與XREAD的0一致)。除了支持CREATE
外,還支持SETID
設置起始ID,DESTROY
銷燬組,DELCONSUMER
刪除組內消費者等操作。
XREADGROUP GROUP mqGroup consumerA COUNT 1 STREAMS mq >
,用於組mqGroup
內消費者consumerA
在隊列mq
中消費,參數>
表示未被組內消費的起始消息,參數count 1
表示獲取一條。語法與XREAD
基本一致,不過是增加了組的概念。
可以進行組內消費的基本原理是,STREAM類型會爲每個組記錄一個最後處理(交付)的消息ID(last_delivered_id),這樣在組內消費時,就可以從這個值後面開始讀取,保證不重複消費。
以上就是消費組的基礎操作。除此之外,消費組消費時,還有一個必須要考慮的問題,就是若某個消費者,消費了某條消息,但是並沒有處理成功時(例如消費者進程宕機),這條消息可能會丟失,因爲組內其他消費者不能再次消費到該消息了。下面繼續討論解決方案。
#6 Pending 等待列表
爲了解決組內消息讀取但處理期間消費者崩潰帶來的消息丟失問題,STREAM
設計了 Pending
列表,用於記錄讀取但並未處理完畢的消息。命令XPENDIING
用來獲消費組或消費內消費者的未處理完畢的消息。演示如下:
127.0.0.1:6379> XPENDING mq mqGroup # mpGroup的Pending情況
1) (integer) 5 # 5個已讀取但未處理的消息
2) "1553585533795-0" # 起始ID
3) "1553585533795-4" # 結束ID
4) 1) 1) "consumerA" # 消費者A有3個
2) "3"
2) 1) "consumerB" # 消費者B有1個
2) "1"
3) 1) "consumerC" # 消費者C有1個
2) "1"
127.0.0.1:6379> XPENDING mq mqGroup - + 10 # 使用 start end count 選項可以獲取詳細信息
1) 1) "1553585533795-0" # 消息ID
2) "consumerA" # 消費者
3) (integer) 1654355 # 從讀取到現在經歷了1654355ms,IDLE
4) (integer) 5 # 消息被讀取了5次,delivery counter
2) 1) "1553585533795-1"
2) "consumerA"
3) (integer) 1654355
4) (integer) 4
# 共5個,餘下3個省略 ...
127.0.0.1:6379> XPENDING mq mqGroup - + 10 consumerA # 在加上消費者參數,獲取具體某個消費者的Pending列表
1) 1) "1553585533795-0"
2) "consumerA"
3) (integer) 1641083
4) (integer) 5
# 共3個,餘下2個省略 ...
每個Pending的消息有4個屬性:
- 消息ID
- 所屬消費者
- IDLE,已讀取時長
- delivery counter,消息被讀取次數
上面的結果我們可以看到,我們之前讀取的消息,都被記錄在Pending列表中,說明全部讀到的消息都沒有處理,僅僅是讀取了。那如何表示消費者處理完畢了消息呢?使用命令 XACK
完成告知消息處理完成,演示如下:
127.0.0.1:6379> XACK mq mqGroup 1553585533795-0 # 通知消息處理結束,用消息ID標識
(integer) 1
127.0.0.1:6379> XPENDING mq mqGroup # 再次查看Pending列表
1) (integer) 4 # 已讀取但未處理的消息已經變爲4個
2) "1553585533795-1"
3) "1553585533795-4"
4) 1) 1) "consumerA" # 消費者A,還有2個消息處理
2) "2"
2) 1) "consumerB"
2) "1"
3) 1) "consumerC"
2) "1"
127.0.0.1:6379>
有了這樣一個Pending機制,就意味着在某個消費者讀取消息但未處理後,消息是不會丟失的。等待消費者再次上線後,可以讀取該Pending列表,就可以繼續處理該消息了,保證消息的有序和不丟失。
此時還有一個問題,就是若某個消費者宕機之後,沒有辦法再上線了,那麼就需要將該消費者Pending的消息,轉義給其他的消費者處理,就是消息轉移。請繼續。
#7 消息轉移
消息轉移的操作時將某個消息轉移到自己的Pending列表中。使用語法XCLAIM
來實現,需要設置組、轉移的目標消費者和消息ID,同時需要提供IDLE(已被讀取時長),只有超過這個時長,才能被轉移。演示如下:
# 當前屬於消費者A的消息1553585533795-1,已經15907,787ms未處理了
127.0.0.1:6379> XPENDING mq mqGroup - + 10
1) 1) "1553585533795-1"
2) "consumerA"
3) (integer) 15907787
4) (integer) 4
# 轉移超過3600s的消息1553585533795-1到消費者B的Pending列表
127.0.0.1:6379> XCLAIM mq mqGroup consumerB 3600000 1553585533795-1
1) 1) "1553585533795-1"
2) 1) "msg"
2) "2"
# 消息1553585533795-1已經轉移到消費者B的Pending中。
127.0.0.1:6379> XPENDING mq mqGroup - + 10
1) 1) "1553585533795-1"
2) "consumerB"
3) (integer) 84404 # 注意IDLE,被重置了
4) (integer) 5 # 注意,讀取次數也累加了1次
以上代碼,完成了一次消息轉移。轉移除了要指定ID外,還需要指定IDLE,保證是長時間未處理的才被轉移。被轉移的消息的IDLE會被重置,用以保證不會被重複轉移,以爲可能會出現將過期的消息同時轉移給多個消費者的併發操作,設置了IDLE,則可以避免後面的轉移不會成功,因爲IDLE不滿足條件。例如下面的連續兩條轉移,第二條不會成功。
127.0.0.1:6379> XCLAIM mq mqGroup consumerB 3600000 1553585533795-1
127.0.0.1:6379> XCLAIM mq mqGroup consumerC 3600000 1553585533795-1
1
2
這就是消息轉移。至此我們使用了一個Pending消息的ID,所屬消費者和IDLE的屬性,還有一個屬性就是消息被讀取次數,delivery counter,該屬性的作用由於統計消息被讀取的次數,包括被轉移也算。這個屬性主要用在判定是否爲錯誤數據上。請繼續看:
#8 壞消息問題,Dead Letter,死信問題
正如上面所說,如果某個消息,不能被消費者處理,也就是不能被XACK,這是要長時間處於Pending列表中,即使被反覆的轉移給各個消費者也是如此。此時該消息的delivery counter就會累加(上一節的例子可以看到),當累加到某個我們預設的臨界值時,我們就認爲是壞消息(也叫死信,DeadLetter,無法投遞的消息),由於有了判定條件,我們將壞消息處理掉即可,刪除即可。刪除一個消息,使用XDEL
語法,演示如下:
# 刪除隊列中的消息
127.0.0.1:6379> XDEL mq 1553585533795-1
(integer) 1
# 查看隊列中再無此消息
127.0.0.1:6379> XRANGE mq - +
1) 1) "1553585533795-0"
2) 1) "msg"
2) "1"
2) 1) "1553585533795-2"
2) 1) "msg"
2) "3"
注意本例中,並沒有刪除Pending中的消息因此你查看Pending,消息還會在。可以執行XACK
標識其處理完畢!
#9 信息監控,XINFO
Stream提供了XINFO來實現對服務器信息的監控,可以查詢:
查看隊列信息
127.0.0.1:6379> Xinfo stream mq
1) "length"
2) (integer) 7
3) "radix-tree-keys"
4) (integer) 1
5) "radix-tree-nodes"
6) (integer) 2
7) "groups"
8) (integer) 1
9) "last-generated-id"
10) "1553585533795-9"
11) "first-entry"
12) 1) "1553585533795-3"
2) 1) "msg"
2) "4"
13) "last-entry"
14) 1) "1553585533795-9"
2) 1) "msg"
2) "10"
消費組信息
127.0.0.1:6379> Xinfo groups mq
1) 1) "name"
2) "mqGroup"
3) "consumers"
4) (integer) 3
5) "pending"
6) (integer) 3
7) "last-delivered-id"
8) "1553585533795-4"
消費者組成員信息
127.0.0.1:6379> XINFO CONSUMERS mq mqGroup
1) 1) "name"
2) "consumerA"
3) "pending"
4) (integer) 1
5) "idle"
6) (integer) 18949894
2) 1) "name"
2) "consumerB"
3) "pending"
4) (integer) 1
5) "idle"
6) (integer) 3092719
3) 1) "name"
2) "consumerC"
3) "pending"
4) (integer) 1
5) "idle"
6) (integer) 23683256
至此,消息隊列的操作說明大體結束!
#10 命令一覽
命令 | 說明 |
---|---|
XACK | 結束Pending |
XADD | 生成消息 |
XCLAIM | 消息轉移 |
XDEL | 刪除消息 |
XGROUP | 消費組管理 |
XINFO | 得到消費組信息 |
XLEN | 消息隊列長度 |
XPENDING | Pending列表 |
XRANGE | 獲取消息隊列中消息 |
XREAD | 消費消息 |
XREADGROUP | 分組消費消息 |
XREVRANGE | 逆序獲取消息隊列中消息 |
XTRIM | 消息隊列容量 |
#11 Stream數據結構,RadixTree,基數樹
Stream
是基於 RadixTree
數據結構實現的。另立話題討論。基數樹,http://www.hellokang.net/algorithm/radix-tree.html
#12 相關產品
很多成熟的MQ產品:
- Disque,https://disquedurinterne.net/
- Kafka,http://kafka.apache.org/
- ActiveMQ,http://activemq.apache.org/
- RockMQ,http://rocketmq.apache.org/
- RabbitMQ,https://www.rabbitmq.com/
- ZeroMQ,http://zeromq.org/