Redis 5.0 Stream 功能简介

简介

Stream 是 Redis 5.0 引入的一种新数据类型,允许消费者等待生产者发送的新数据,还引入了消费者组概念,组之间数据是相同的(前提是设置的偏移量一样),组内的消费者不会拿到相同数据。这种概念和 Kafka 很雷同。

Redis Stream 的结构如上图所示,它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 streaID 和对应的内容。消息是持久化的,Redis 重启后,内容还在。

每个 Stream 都有唯一的名称,它就是 Redis 的 key,在我们首次使用xadd指令追加消息时自动创建。

每个 Stream 都可以挂多个消费组,每个消费组会有个游标last_delivered_id在 Stream 数组之上往前移动,表示当前消费组已经消费到哪条消息了。每个消费组都有一个 Stream 内唯一的名称,消费组不会自动创建,它需要单独的指令xgroup create进行创建,需要指定从 Stream 的某个消息 ID 开始消费,这个 ID 用来初始化last_delivered_id变量。

每个消费组(Consumer Group)的状态都是独立的,相互不受影响。也就是说同一份 Stream 内部的消息会被每个消费组都消费到。

同一个消费组(Consumer Group)可以挂接多个消费者(Consumer),这些消费者之间是竞争关系,任意一个消费者读取了消息都会使游标last_delivered_id往前移动。每个消费者者有一个组内唯一名称。

消费者(Consumer)内部会有个状态变量pending_ids,它记录了当前已经被客户端读取的消息,但是还没有 ACK。如果客户端没有 ACK,这个变量里面的消息 ID 会越来越多,一旦某个消息被 ACK,它就开始减少。这个 pending_ids 变量在 Redis 官方被称之为PEL,也就是Pending Entries List,这是一个很核心的数据结构,它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理。

特点

  • 与 Redis 的 pub/sub 不同,pub/sub 多个客户端是收到相同的数据,而 Stream 的多个客户端是竞争关系,每个客户端收到的数据是不相同的。
  • pub/sub 中一旦触发数据获取,不会记录下上一次拿的位置,意味着客户端无法重复去拿以前的数据,而 blpop 方式一旦 pop,数据就会永久的删除,也无法重复去拿以前的数据。而 Stream 会永久的存放数据,并且客户端会保留上一次拿的 ID,甚至通过修改 ID 可以拿回以前的数据。和 Kafka 的机制类似。
  • Stream 提供了消费者组(Kafka 也有),不同组接收到的数据完全一样(前提是条件一样),但是组内的消费者则是竞争关系(还是和 Kafka 一样)。
  • 可以设置为阻塞与非阻塞模式。
  • 多客户端时,遵循 FIFO 特性。

消息ID

消息ID的形式是timestampInMillis-sequence,例如1527846880572-5,它表示当前的消息在毫米时间戳1527846880572时产生,并且是该毫秒内产生的第 5 条消息。消息 ID 可以由服务器自动生成,也可以由客户端自己指定,但是形式必须是整数-整数,而且必须是后面加入的消息的 ID 要大于前面的消息 ID。

消息内容

消息内容就是键值对,形如 hash 结构的键值对,这没什么特别之处。

增删改查

  1. xadd 追加消息
  2. xdel 删除消息,这里的删除仅仅是设置了标志位,不影响消息总长度
  3. xrange 获取消息列表,会自动过滤已经删除的消息
  4. xlen 消息长度
  5. del 删除 Stream
 
  1. # *号表示服务器自动生成 ID,后面顺序跟着一堆 key/value
  2. 127.0.0.1:6379> xadd codehole * name laoqian age 30 # 名字叫 laoqian,年龄 30 岁
  3. 1527849609889-0 # 生成的消息ID
  4. 127.0.0.1:6379> xadd codehole * name xiaoyu age 29
  5. 1527849629172-0
  6. 127.0.0.1:6379> xadd codehole * name xiaoqian age 1
  7. 1527849637634-0
  8. 127.0.0.1:6379> xlen codehole
  9. (integer) 3
  10. 127.0.0.1:6379> xrange codehole - + # - 表示最小值, + 表示最大值
  11. 127.0.0.1:6379> xrange codehole - +
  12. 1) 1) 1527849609889-0
  13. 2) 1) "name"
  14. 2) "laoqian"
  15. 3) "age"
  16. 4) "30"
  17. 2) 1) 1527849629172-0
  18. 2) 1) "name"
  19. 2) "xiaoyu"
  20. 3) "age"
  21. 4) "29"
  22. 3) 1) 1527849637634-0
  23. 2) 1) "name"
  24. 2) "xiaoqian"
  25. 3) "age"
  26. 4) "1"
  27. 127.0.0.1:6379> xrange codehole 1527849629172-0 + # 指定最小消息 ID 的列表
  28. 1) 1) 1527849629172-0
  29. 2) 1) "name"
  30. 2) "xiaoyu"
  31. 3) "age"
  32. 4) "29"
  33. 2) 1) 1527849637634-0
  34. 2) 1) "name"
  35. 2) "xiaoqian"
  36. 3) "age"
  37. 4) "1"
  38. 127.0.0.1:6379> xrange codehole - 1527849629172-0 # 指定最大消息 ID 的列表
  39. 1) 1) 1527849609889-0
  40. 2) 1) "name"
  41. 2) "laoqian"
  42. 3) "age"
  43. 4) "30"
  44. 2) 1) 1527849629172-0
  45. 2) 1) "name"
  46. 2) "xiaoyu"
  47. 3) "age"
  48. 4) "29"
  49. 127.0.0.1:6379> xdel codehole 1527849609889-0
  50. (integer) 1
  51. 127.0.0.1:6379> xlen codehole # 长度不受影响
  52. (integer) 3
  53. 127.0.0.1:6379> xrange codehole - + # 被删除的消息没了
  54. 1) 1) 1527849629172-0
  55. 2) 1) "name"
  56. 2) "xiaoyu"
  57. 3) "age"
  58. 4) "29"
  59. 2) 1) 1527849637634-0
  60. 2) 1) "name"
  61. 2) "xiaoqian"
  62. 3) "age"
  63. 4) "1"
  64. 127.0.0.1:6379> del codehole # 删除整个 Stream
  65. (integer) 1

独立消费

我们可以在不定义消费组的情况下进行 Stream 消息的独立消费,当 Stream 没有新消息时,甚至可以阻塞等待。Redis 设计了一个单独的消费指令xread,可以将 Stream 当成普通的消息队列(list)来使用。使用xread时,我们可以完全忽略消费组(Consumer Group)的存在,就好比 Stream 就是一个普通的列表(list)。

 
  1. # 从 Stream 头部读取两条消息
  2. 127.0.0.1:6379> xread count 2 streams codehole 0-0
  3. 1) 1) "codehole"
  4. 2) 1) 1) 1527851486781-0
  5. 2) 1) "name"
  6. 2) "laoqian"
  7. 3) "age"
  8. 4) "30"
  9. 2) 1) 1527851493405-0
  10. 2) 1) "name"
  11. 2) "yurui"
  12. 3) "age"
  13. 4) "29"
  14. # 从 Stream 尾部读取一条消息,毫无疑问,这里不会返回任何消息
  15. 127.0.0.1:6379> xread count 1 streams codehole $
  16. (nil)
  17. # 从尾部阻塞等待新消息到来,下面的指令会堵住,直到新消息到来
  18. 127.0.0.1:6379> xread block 0 count 1 streams codehole $
  19. # 我们从新打开一个窗口,在这个窗口往 Stream 里塞消息
  20. 127.0.0.1:6379> xadd codehole * name youming age 60
  21. 1527852774092-0
  22. # 再切换到前面的窗口,我们可以看到阻塞解除了,返回了新的消息内容
  23. # 而且还显示了一个等待时间,这里我们等待了 93s
  24. 127.0.0.1:6379> xread block 0 count 1 streams codehole $
  25. 1) 1) "codehole"
  26. 2) 1) 1) 1527852774092-0
  27. 2) 1) "name"
  28. 2) "youming"
  29. 3) "age"
  30. 4) "60"
  31. (93.11s)

客户端如果想要使用xread进行顺序消费,一定要记住当前消费到哪里了,也就是返回的消息ID。下次继续调用xread时,将上次返回的最后一个消息 ID 作为参数传递进去,就可以继续消费后续的消息。

block 0 表示永远阻塞,直到消息到来,block 1000表示阻塞 1s,如果 1s 内没有任何消息到来,就返回nil

 
  1. 127.0.0.1:6379> xread block 1000 count 1 streams codehole $
  2. (nil)
  3. (1.07s)

创建消费组

Stream 通过xgroup create指令创建消费组(Consumer Group),需要传递起始消息 ID 参数用来初始化last_delivered_id变量。

 
  1. 127.0.0.1:6379> xgroup create codehole cg1 0-0 # 表示从头开始消费
  2. OK
  3. # $ 表示从尾部开始消费,只接受新消息,当前 Stream 消息会全部忽略
  4. 127.0.0.1:6379> xgroup create codehole cg2 $
  5. OK
  6. 127.0.0.1:6379> xinfo codehole # 获取 Stream 信息
  7. 1) length
  8. 2) (integer) 3 # 共 3 个消息
  9. 3) radix-tree-keys
  10. 4) (integer) 1
  11. 5) radix-tree-nodes
  12. 6) (integer) 2
  13. 7) groups
  14. 8) (integer) 2 # 两个消费组
  15. 9) first-entry # 第一个消息
  16. 10) 1) 1527851486781-0
  17. 2) 1) "name"
  18. 2) "laoqian"
  19. 3) "age"
  20. 4) "30"
  21. 11) last-entry # 最后一个消息
  22. 12) 1) 1527851498956-0
  23. 2) 1) "name"
  24. 2) "xiaoqian"
  25. 3) "age"
  26. 4) "1"
  27. 127.0.0.1:6379> xinfo groups codehole # 获取 Stream 的消费组信息
  28. 1) 1) name
  29. 2) "cg1"
  30. 3) consumers
  31. 4) (integer) 0 # 该消费组还没有消费者
  32. 5) pending
  33. 6) (integer) 0 # 该消费组没有正在处理的消息
  34. 2) 1) name
  35. 2) "cg2"
  36. 3) consumers # 该消费组还没有消费者
  37. 4) (integer) 0
  38. 5) pending
  39. 6) (integer) 0 # 该消费组没有正在处理的消息

消费

Stream 提供了xreadgroup指令可以进行消费组的组内消费,需要提供消费组名称、消费者名称和起始消息 ID。它同xread一样,也可以阻塞等待新消息。读到新消息后,对应的消息 ID 就会进入消费者的PEL(正在处理的消息)结构里,客户端处理完毕后使用xack指令通知服务器,本条消息已经处理完毕,该消息 ID 就会从 PEL 中移除。

 
  1. # >号表示从当前消费组的last_delivered_id后面开始读
  2. # 每当消费者读取一条消息,last_delivered_id变量就会前进
  3. 127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >
  4. 1) 1) "codehole"
  5. 2) 1) 1) 1527851486781-0
  6. 2) 1) "name"
  7. 2) "laoqian"
  8. 3) "age"
  9. 4) "30"
  10. 127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >
  11. 1) 1) "codehole"
  12. 2) 1) 1) 1527851493405-0
  13. 2) 1) "name"
  14. 2) "yurui"
  15. 3) "age"
  16. 4) "29"
  17. 127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 2 streams codehole >
  18. 1) 1) "codehole"
  19. 2) 1) 1) 1527851498956-0
  20. 2) 1) "name"
  21. 2) "xiaoqian"
  22. 3) "age"
  23. 4) "1"
  24. 2) 1) 1527852774092-0
  25. 2) 1) "name"
  26. 2) "youming"
  27. 3) "age"
  28. 4) "60"
  29. # 再继续读取,就没有新消息了
  30. 127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >
  31. (nil)
  32. # 那就阻塞等待吧
  33. 127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole >
  34. # 开启另一个窗口,往里塞消息
  35. 127.0.0.1:6379> xadd codehole * name lanying age 61
  36. 1527854062442-0
  37. # 回到前一个窗口,发现阻塞解除,收到新消息了
  38. 127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole >
  39. 1) 1) "codehole"
  40. 2) 1) 1) 1527854062442-0
  41. 2) 1) "name"
  42. 2) "lanying"
  43. 3) "age"
  44. 4) "61"
  45. (36.54s)
  46. 127.0.0.1:6379> xinfo groups codehole # 观察消费组信息
  47. 1) 1) name
  48. 2) "cg1"
  49. 3) consumers
  50. 4) (integer) 1 # 一个消费者
  51. 5) pending
  52. 6) (integer) 5 # 共5条正在处理的信息还有没有ack
  53. 2) 1) name
  54. 2) "cg2"
  55. 3) consumers
  56. 4) (integer) 0 # 消费组cg2没有任何变化,因为前面我们一直在操纵cg1
  57. 5) pending
  58. 6) (integer) 0
  59. # 如果同一个消费组有多个消费者,我们可以通过xinfo consumers指令观察每个消费者的状态
  60. 127.0.0.1:6379> xinfo consumers codehole cg1 # 目前还有1个消费者
  61. 1) 1) name
  62. 2) "c1"
  63. 3) pending
  64. 4) (integer) 5 # 共5条待处理消息
  65. 5) idle
  66. 6) (integer) 418715 # 空闲了多长时间ms没有读取消息了
  67. # 接下来我们ack一条消息
  68. 127.0.0.1:6379> xack codehole cg1 1527851486781-0
  69. (integer) 1
  70. 127.0.0.1:6379> xinfo consumers codehole cg1
  71. 1) 1) name
  72. 2) "c1"
  73. 3) pending
  74. 4) (integer) 4 # 变成了5条
  75. 5) idle
  76. 6) (integer) 668504
  77. # 下面ack所有消息
  78. 127.0.0.1:6379> xack codehole cg1 1527851493405-0 1527851498956-0 1527852774092-0 1527854062442-0
  79. (integer) 4
  80. 127.0.0.1:6379> xinfo consumers codehole cg1
  81. 1) 1) name
  82. 2) "c1"
  83. 3) pending
  84. 4) (integer) 0 # pel空了
  85. 5) idle
  86. 6) (integer) 745505

Stream 消息太多怎么办

读者很容易想到,要是消息积累太多,Stream 的链表岂不是很长,内容会不会爆掉就是个问题了。xdel指令又不会删除消息,它只是给消息做了个标志位。

Redis 自然考虑到了这一点,所以它提供了一个定长 Stream 功能。在xadd的指令提供一个定长长度maxlen,就可以将老的消息干掉,确保最多不超过指定长度。

 
  1. 127.0.0.1:6379> xlen codehole
  2. (integer) 5
  3. 127.0.0.1:6379> xadd codehole maxlen 3 * name xiaorui age 1
  4. 1527855160273-0
  5. 127.0.0.1:6379> xlen codehole
  6. (integer) 3

我们看到 Stream 的长度被砍掉了。

消息如果忘记 ACK 会怎样

Stream 在每个消费者结构中保存了正在处理中的消息 ID 列表 PEL,如果消费者收到了消息处理完了但是没有回复 ACK,就会导致 PEL 列表不断增长,如果有很多消费组的话,那么这个 PEL 占用的内存就会放大。

PEL 如何避免消息丢失

在客户端消费者读取 Stream 消息时,Redis 服务器将消息回复给客户端的过程中,客户端突然断开了连接,消息就丢失了。但是 PEL 里已经保存了发出去的消息 ID。待客户端重新连上之后,可以再次收到PEL中的消息 ID 列表。不过此时xreadgroup的起始消息 ID 不能为参数,而必须是任意有效的消息 ID,一般将参数设为 0-0,表示读取所有的 PEL 消息以及自last_delivered_id之后的新消息。

你可能感兴趣的:(Redis 5.0 Stream 功能简介)