小道消息-PubSub
消息多播
消息多播允许生产者只生产一次消息,由中间件负责将消息复制到多个消息队列,每个消息队列由相应的消费组进行消费。
PubSub 常见指令
> subscribe codehole.image codehole.text codehole.blog #同时订阅三个主题,会有三条订阅成功反馈消息
1) "subscribe"
2) "codehole.image"
3) (integer) 1
1) "subscribe"
2) "codehole.text"
3) (integer) 2
1) "subscribe"
2) "codehole.blog"
3) (integer) 3
生产者向三个主题发布消息
> publish codehole.image https://www.google.com/dudo.png
(integer) 1
> publish codehole.text "你好,欢迎加入码洞"
(integer) 1
> publish codehole.blog '{"content":"hello,everyone","title":"welcome"}'
(integer) 1
redis提供了模式订阅功能Pattern Subscribe,这样就可以一次订阅多个主题,即使生产者新增了同模式的主题,消费者也可以立即收到消息。
# 用模式匹配一次订阅多个主题,主题以codehole. 字符开头的消息都可以收到
> psubscribe codehole.*
1) "psubscribe"
2) "codehole.*"
3) (integer) 1
消息结构
消费者消息输出时是如下的字典形式
{'pattern':None,'type':'subscribe','channel':'codehole','data':1L}
{'pattern':None,'type':'message','channel':'codehole','data':'golang comes'}
字段含义解释:
- data 消息的内容,一个字符串
- channel 当前订阅的主题名称
- type 消息的类型。如果是一个普通的消息,那么类型就是message;如果是控制消息,比如订阅指令的反馈,它的类型就是subscribe;如果是模式订阅的反馈,它的类型就是pubscribe;此外还有取消订阅指令的反馈unsubscribe和punsubscribe
- pattern 表示当前消息是使用哪种模式订阅到的.如果是通过subscribe 指令订阅的,那么这个字段就是空
PubSub的缺点
PubSub的生产者传递过来一个消息,Redis会直接找到对应的消费者传递过去。如果一个消费者都没有,那么消息会被直接丢弃.如果开始有三个消费者,一个消费者突然挂掉了,生产者会继续发送消息,另外两个消费者可以持续收到消息,但是当挂掉的消费者重新连上的时候,在断连期间生产者发送的消息,对于这个消费者来说就是彻底丢失了。
耳听八方–Stream
Redis 5.0 新增加了一个数据结构Stream,它是一个新的强大的支持多播的可持久化消息队列,其借鉴了Kafka的设计
Redis Stream 的结构如图所示,它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的ID和对应的内容.消息是持久化的,Redis重启后,内容还在。
每个Stream都有唯一的名称,它就是Redis的key,在我们首次使用xadd指令追加消息时自动创建。
每个Stream都可以挂多个消费组(Consumer Group),每个消费组都会有个游标last_delivered_id在Stream数组之上往前移动,表示当前消费组已经消费到哪条消息了.每个消费组都有一个Stream内唯一的名称,消费组不会自动创建,它需要单独的指令xgroup create进行创建,需要指定从Stream 的某个消息ID开始消费,这个ID用来初始化last_delivered_id变量。
同一个消费者组可以挂接多个消费者,这些消费者之间是竞争关系。任意一个消费者读取了消息都会使游标last_delivered_id 往前移动。每个消费者组的状态都是独立的,相互不受影响。
消费者内部会有一个状态变量 pending_ids,它记录了当前已经被客户端读取,但是还没有ack的消息。如果客户端没有ack,这个变量里面的消息ID就会越来越多,一旦某个消息被ack,它就开始减少。这个 pending_ids 变量在redis官方被称为PEL,也就是Pending Entries List,这是一个核心的数据结构,它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了而没被处理。
消息ID
消息ID的形式是timestampInMillis-sequence,例如 1527846880572-5,它表示当前的消息在毫秒时间戳 1527846880572时产生,并且是该毫秒内产生的第5条消息。消息ID可以由服务器自动生成,也可以由客户端自己指定,但是形式必须是"整数-整数",而且后面加入的消息ID必须要大于前面的消息ID
增删改查
1. xadd: 向Stream追加消息
2. xdel: 从stream中删除消息,这里的删除仅仅是设置标志位,不影响消息总长度。
3. xrange: 获取stream中的消息列表,会自动过滤已经删除的消息
4. xlen: 获取stream消息长度
5. del: 删除整个stream消息列表中的所有消息
> xadd codehole * name laoqian age 30 # *号表示服务器自动生成ID,后面顺序跟着key、value
1527846880572-0 # 生成的消息ID
> xlen codehole
(integer) 3
# - 表示最小值, +表示最大值
> xrange codehole - +
1) 1) 1527846880572-0
2) 1) "name"
2) "laoqian"
3) "age"
4) "30"
> xdel codehole 1527846880572-0 # 删除指定ID的消息
> xlen codehole
(integer) 3 #长度不受影响,但是用xrange查看时 被删除的消息没了
> del codehole # 删除整个Stream
(integer) 1
独立消费
redis 设计了一个单独的消费指令xread,可以将stream当成普通的消息队列(list)来使用。
# 从stream头部读取两条消息
> xread count 2 streams codehole 0-0
# 从stream尾部读取一条消息
> xread count 1 streams codehole $
# 从尾部阻塞等待新消息到来
> xread block 0 count 1 streams codehole $
# block 0 表示永远阻塞,直到消息到来;block 1000表示阻塞1s,如果1s内没有任何消息到来,就返回nil
创建消费组
stream 通过xgroup create 指令创建消费组
# 表示从头部开始消费
> xgroup create codehole cg1 0-0
OK
# $ 表示从尾部开始消费,只接受新消息,当前Stream消息会全部忽略
> xgroup create codehole cg2 $
OK
# 获取stream信息
> xinfo stream codehole
# 获取stream的消费组信息
> xinfo groups codehole
消费
Stream 提供了xreadgroup指令可以进行消费组的组内消费,需要提供消费组名称,消费者名称和起始消息ID。它同xread一样,也可以阻塞等待新消息。读到新消息后,对应的消息ID就会进入消费者的PEL(正在处理的消息)结构里,客户端处理完毕后使用xack指令通知服务器,本条消息已经处理完毕,该消息ID就会从PEL中移除。
# > 表示从当前消费组的 last_delivered_id 后面开始读
# 每当消费者读取一条消息, last_delivered_id 变量就会前进
> xreadgroup GROUP cg1 c1 count 1 streams codehole >
# 观察消费组信息
> xinfo groups codehole
# ack一条消息
> xack codehole cg1 1527846880572-0
stream 消息太多怎么办
提供了一个定长参数maxlen,可以将老的消息干掉,确保链表不超过指定长度
xadd codehole maxlen 3 * name xiaorui age 1