综述
我们先看看消息队列的消息存取到底有哪些需求吧:
需求1:消息保序:由于消费者是异步处理消息,但是消费者需要按照生产者发送消息的顺序来处理消息,避免后发送的消息被先处理了。
需求2:重复消息处理:有时候可能因为网络堵塞出现消息重传的情况,消费者需要保证幂等性。换句话说,对于同一条消息,消费者收到一次的处理结果和收到多次的处理结果是一致的。
需求3:消息可靠性保证:有时候消费者在处理消息时可能出现因故障或宕机导致消息没有处理完成的情况,因此消息队列需要提供消息可靠性的保证。换句话说,当消费者重启之后,可以重新读取消息并再次进行处理。
消息队列的目的是在分布式的系统间进行通信,通信方式是通过网络传输的方式,通过网络传输就有消息丢失、重复发送消息、不同消息到达顺序混乱的可能。
所以消息队列应用的三大需求是:1、消息保序;2、重复消息处理;3、消息可靠性保证。
对应处理方案是:1、消息数据有序存取;2、消息数据具有全局唯一编号;3、消息数据在未消费完宕机恢复时继续消费,消费完成后被删除。
Redis如何实现MQ的需求?
总体来说,Redis提供了两种解决方案:一是基于List的消息队列解决方案,另一种则是基于Stream的消息队列解决方案。
基于List的MQ解决方案
首先,对于需求1-消息保序,List是按照先进先出的顺序对数据进行存取的,因此使用List作为消息队列保存消息可以满足需求1。具体实现就是LPUSH+RPOP/BRPOP。
潜在风险点:即使没有新消息写入List,消费者也需要不停地调用RPOP命令,这就会导致消费者程序的CPU一直消耗在执行RPOP命令上,带来不必要的性能损失。因此,Redis提供了BRPOP命令,提供阻塞式读取,即在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。
其次,对于需求2-重复消息处理,List本身是不会为每个消息生成ID号的,所以,消息的全局唯一ID号就需要生产者程序在发送消息前自动生成,生成之后,在LPUSH时需要把这个全局唯一ID包含进去。
例如:将一条全局 ID 为 101030001、库存量为 5 的消息插入Redis消息队列
LPUSH mq "101030001:stock:5"
(integer) 1
最后,对于需求3-消息可靠性保证,List本身在读取一条消息后就不会再留存这条消息了,所以为了留存消息,List提供了BRPOPLPUSH命令,即让消费者程序从一个List中读取消息,同时再把这个消息插入到另一个List(可以理解为备份List)留存。这样一来,如果消费者程序读了消息但没能正常处理,等它重启后,就可以从备份 List 中重新读取消息并进行处理了。
综上所述,基于List可以满足对MQ的三大需求,但是,使用List还是存在一个问题:生产者消息发送很快,但是消费者处理消息的速度很慢,这就可能会导致List中的消息越积越多,给Redis的内存带来很大的压力。在Redis 5.0开始,提供了Stream数据类型,它支持多个消费者程序组成一个消费组,一起分担消息处理压力。
基于Stream的MQ解决方案
Streams 是 Redis 专门为消息队列设计的数据类型,它提供了丰富的消息队列操作命令。
(1)XADD
插入消息,保证有序,可以自动生成全局唯一ID
(2)XREAD
读取消息,可以按ID读取数据
(3)XREADGROUP
按消费组形式读取消息
(4)XPENDING + XACK
XPENDING用来查询每个消费组内所有消费者已读取但尚未确认的消息
XACK用来向消息队列确认消息处理已完成
下面一一讲解各个核心命令操作:
XADD
XADD 命令可以往消息队列中插入新消息,消息的格式是键 - 值对形式。对于插入的每一条消息,Stream 可以自动为其生成一个全局唯一的 ID。
例如:往一个名为mqstream的队列中插入一条消息,key=repo, value=5,中间的*表示让Redis为插入的数据自动生成一个全局唯一的ID号,这里是1599203861727-0,它的格式是当前服务器时间(精确到毫秒)+序号。
XADD mqstream * repo 5
"1599203861727-0"
XREAD
XREAD 在读取消息时,可以指定一个消息 ID,并从这个消息 ID 的下一条消息开始进行读取。