Redis Stream JAVA 操作

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

目錄

Stream 類型

#1 概述

#2 追加新消息,XADD,生產消息

#3 從消息隊列中獲取消息,XREAD,消費消息

#4 消息ID說明

#5 消費者組模式,consumer group

#6 Pending 等待列表

#7 消息轉移

#8 壞消息問題,Dead Letter,死信問題

#9 信息監控,XINFO

#10 命令一覽

#11 Stream數據結構,RadixTree,基數樹

#12 相關產品


#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個屬性:

  1. 消息ID
  2. 所屬消費者
  3. IDLE,已讀取時長
  4. 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/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章