这是大家最常用的队列方式,就是在一个list上用lpush & rpop,如下图所示:
由于空队列的问题,要引入for循环加上一定的sleep时间,伪代码如下:
for {
if msg:=redis.rpop();msg!=nil{
handle(msg)
}else{
time.sleep(1000)
}
}
这种方案可能存在1s处理不及时的风险(虽说在大多场景下基本没有影响)
不过redis有block算子,通过brpop实现阻塞式拉取,可以及时获得数据,伪代码如下:
for {
# 超时时间为 0,代表无限等待
if msg:=redis.brpop(0);msg!=nil{
handle(msg)
}
}
看起来很完美,解决了不处理不及时的问题,但由于redis client把超时时间设置成0后,redis server在长时间没有接受到消息的情况下,可能会判定该client为无效的链接,从而强制踢下线,所以在消息不是很密集的时候,直接设为0时,还是有一定的风险,建议保留非0的最小等待值(1s即可),伪代码如下:
for {
if msg:=redis.brpop(1000);msg!=nil{
handle(msg)
}
}
既保证了实时性,又避免了链接断开
这样一个最简单的队列就有了,但是要注意这里并不是可靠的队列,主要是消息丢失问题:由于缺失ack机制,消费端在拉到(rpop)消息后宕机(或新版本上线)的话,该消息是很大概率缺失的
其次,我们在看一些消息队列的特性:
顾名思义,是redis专门解决分发/订阅问题而产生的命令—— publish & subscribe
# 生产者
127.0.0.1:6379> publish queue 1
(integer) 1
# 消费者 1
127.0.0.1:6379> subscribe queue
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "queue"
3) (integer) 1
1) "message"
2) "queue"
3) "1"
# 消费者 2
127.0.0.1:6379> subscribe queue
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "queue"
3) (integer) 1
1) "message"
2) "queue"
3) "1"
其主要解决了支持多组消费者的问题,如下所示:
但由于pub/sub本身是没有持久化的能力,只是数据的实时转发,会造成以下几个问题:
所以,这就要求消费者一定要早于生产者先上线,否则会丢失消息
其次,每个订阅关系内部是个buffer,由于buffer是有上限的,一旦超出上限,redis就会强制让消费者下线,造成消息丢失
这里我们看出基于list的队列是pull的模型,而pub/sub是基于push的模型,先推到buffer,然后等消费者来取
总体来说十分鸡肋,没啥意义,不要考虑该方案
注意这个feature是在redis 5.0才有的
Stream 通过 xadd 和 xreadgroup来实现消息的生产与消费
# 生产者
127.0.0.1:6379> xadd queue * k1 v1
1636289446933-0
127.0.0.1:6379> xadd queue * k2 v2
1636295029388-0
127.0.0.1:6379> xadd queue * k3 v3
1636291571597-0
# 消费者 1
127.0.0.1:6379> xreadgroup group g1 c1 COUNT 1 streams queue >
1) 1) "queue"
2) 1) 1) "1636289446933-0"
2) 1) "k1"
2) "v1"
127.0.0.1:6379> xreadgroup group g1 c1 COUNT 1 streams queue >
1) 1) "queue"
2) 1) 1) "1636295029388-0"
2) 1) "k2"
2) "v2"
# 消费者 2
127.0.0.1:6379> xreadgroup group g1 c2 COUNT 1 streams queue >
1) 1) "queue"
2) 1) 1) "1636291571597-0"
2) 1) "k3"
2) "v3"
利用 xack 和 xreadgroup 实现消息的应答与恢复
# 手动 ack
127.0.0.1:6379> xack queue g1 1636289446933-0
(integer) 1
# 查询尚未提交位于 pending 中的消息
127.0.0.1:6379> xreadgroup group g1 c1 COUNT 1 streams queue 0
1) 1) "queue"
2) 1) 1) "1636295029388-0"
2) 1) "k2"
2) "v2"
大多数恢复消费是基于以下的代码实现的
for {
# 从未ack的消息开始消费
id:=getLastUnackIDByPending()
if id=0 {
# 从未推送的消息开始消费
id=">"
}
msg:=xreadgroup(lastUnAckID)
handle(msg)
xack(msg)
}
在消费者不变(数量、唯一id等)的情况下,上述是一个很便捷的方案;但是如果发生变动,就需要引入额外的定时轮询的方案,或者保证消费者一致性的zookeeper方案了,较为复杂,这里不过多展开了,后面会专门开一个stream的专题
我们将redis的队列与专业的队列rabbitMQ、kafka对比一下,整体盘点以下两个特性:
这是一个整体的话题,需要生产者、消费者、中间件三方配合才能实现
1. 生产者丢失消息的情况
所以生产者想要不丢消息就只能重试,这时候消费者就要考虑重试消息的处理,实现幂等性
从这点看,生产者不丢消息与整个中间件无关,完全是业务实现的问题,是否考虑了以上的异常情况
2. 消费者丢失消息的情况
主要是消费者宕机,取出后没有回执
这种场景下,就需要中间件提供一种ack机制,确保哪些消息已经消费掉了,从而保证消息不丢失
这一点redis的stream与kafka、rabbitMQ都是一致的,都有完善的ack机制
3. 中间丢失消息的情况
这其实就是中间件的实现方式了
根据大佬所说,redis存在两个风险点:
而kafka、rabbitMQ则是通过一次写入,多个节点同时ack,才认为写入成功,进一步加强了消息的可靠性