什么是消息队列?
消息队列(Message Queue/Broker),顾名思义:存放消息的队列。是分布式系统中重要的组件,利用高效可靠的消息传递机制进行与平台解耦的数据通信。主要解决应用耦合、异步处理、流量削锋等问题。
Redis实现消息队列
- 基于List数据结构实现消息队列
- 基于发布订阅机制实现消息队列
- 基于Stream数据结构实现消息队列
基于List数据结构实现消息队列
Redis的List结构基于双向链表实现,所以基于lpush和rpop或者rpush和lpop
指令即可,同时支持阻塞获取指令BRPOP或BLPOP
。
127.0.0.1:6379> LPUSH queue 1 2 3 4
(integer) 4
127.0.0.1:6379> Rpop queue
"1"
127.0.0.1:6379> Rpop queue
"2"
127.0.0.1:6379> Rpop queue
"3"
127.0.0.1:6379> Rpop queue
"4"
127.0.0.1:6379> Rpop queue
(nil)
由于列队的特性是:先进先出,所以只要从list结构一端进,另一端出即可。上述demo中时使用LPUSH和RPOP
实现简单队列的效果, 当队列中不存在元素时,返回nil。
若要支持阻塞获取队列元素时,需要结合LPUSH和BRPOP
命令使用。
使用BRPOP
命令会阻塞当前客户端,直到队列中存在元素或等待时间达到。
127.0.0.1:6379> LPUSH queue 15 16
(integer) 2
127.0.0.1:6379> BRPOP queue 100
1) "queue" --队列名称
2) "15" --消息体
(5.16s) --获取消息等待的时间
优点:
- 利用Redis存储,不受限于JVM内存上限基于Redis的持久化机制,
- 数据安全性有保证
- 可以满足消息有序性
缺点:
- 不能保证消息被消费(无法避免消息丢失)
- 不能重复消费,一旦消费就被删除(无法被多个消费者 消费同一条消息)
基于发布订阅机制实现消息队列
Pubsub (发布订阅)是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。
- 一个channel可以有一个或多个订阅者
- 一个发布者可向多个channel发送消息
PUBLISH 发布
PUBLISH channel message
summary: Post a message to a channel
retuen: the number of clients that received the message.
PUBLISH channel(通道name)message(消息体)
返回:接受该消息的客户端数量,包括匹配模式。
SUBSCRIBE 订阅
SUBSCRIBE channel [channel …]
summary: Listen for messages published to the given channels
127.0.0.1:6379> SUBSCRIBE channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
...阻塞等待channel中的消息...
当channel收到消息时:
127.0.0.1:6379> SUBSCRIBE channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "message"
2) "channel1"
3) "k1=v1"
PSUBSCRIBE 模式匹配订阅
PSUBSCRIBE pattern [pattern …]
summary: Listen for messages published to channels matching the given patterns
支持的模式匹配规则:
-
h?llo subscribes to hello, hallo and hxllo
-
h*llo subscribes to hllo and heeeello
-
h[ae]llo subscribes to hello and hallo, but not hillo
'?' :匹配单个字符 '*' :匹配多个字符 '[]':匹配自定义[]内部字符
127.0.0.1:6379> PSUBSCRIBE chan* Reading messages... (press Ctrl-C to quit) 1) "psubscribe" 2) "chan*" 3) (integer) 1 1) "pmessage" 2) "chan*" --返回匹配的规则 3) "channel1"--返回匹配规则的通道name 4) "k1=v1" --返回消息体
优点:
- 采用发布订阅模型,支持多生产、多消费
- 阻塞读取消息(但无法设置阻塞时间)
缺点:
- 不支持数据持久化
- 无法保证数据被消费(无法避免消息丢失)
- 消息堆积有上限,超出限制将丢失旧的数据
基于Stream数据结构实现消息队列
stream单消费者模式
xlen:查询消息长度
XLEN key
summary: Return the number of entries in a stream
xlen 队列名称
xadd :添加消息
XADD key [NOMKSTREAM] [MAXLEN|MINID [=|~] threshold [LIMIT count]] *|ID field value [field value …]
-
key(队列名称);
-
[NOMKSTREAM](若指定,当队列不存在则不创建。默认创建);
-
[MAXLEN|MINID [=|~] threshold [LIMIT count]] 设置消息队列的最大消息数量,若超过数量,但未被消费,则会抛弃旧的数据;
-
*|ID 表示自动生成唯一key,是unix毫秒时间戳-同一毫秒的序列号。也可以自定义自增id。
-
field value [field value …] 消息key1 消息value1 消息key2 消息value2 …消息体键值对
DEMO:
127.0.0.1:6379> xadd stream1 * k1 v1 k2 v2 "1692755653814-0" -- *,代表redis自增ID,由时间戳-递增数字
xread:读取消息
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key …] ID [ID …]]
-
count n 可选,读取n条数据
-
block 0 可选,阻塞等待毫秒数。block设置为0时表示没有消息就一直阻塞,不设置block就是非阻塞模式,拿不拿到都返回,拿不到返回nil
-
streams key [key …] 必填,指定一个或多个队列名称
- 设置ID [ID …]为
指定消息ID
,将**(阻塞)** 获取该消息ID之后的消息 - 设置ID [ID …] 为
$
,将**(阻塞)** 获取队列最新消息 - 设置ID [ID …] 为
0
,将**(阻塞)** 获取队列第一个消息及之后的消息
DEMO:
127.0.0.1:6379> xread count 1 block 1000 streams stream1 $ (nil) --阻塞时间达到,若channel没有消息则返回nil (1.02s) --返回阻塞等待的时间 127.0.0.1:6379> xread count 1 block 10000 streams stream1 $ 1) 1) "stream1" --队列名称 2) 1) 1) "1692755905302-0" --消息ID 2) 1) "k1" --消息体 2) "v1" 3) "k2" 4) "v2" (3.20s)
- 设置ID [ID …]为
优点:
- 采用stream机数据结构,支持多生产、多消费
- 支持数据持久化(消息可回溯)
- 消息可阻塞读取(可设置阻塞时间)
缺点:
- 无法保证数据被消费(无法避免消息丢失)——未标记消息已处理,也没有ack确认机制,无法保证消息最少被消费一次。
stream消费者组模式
消费者组 (Consumer Group): 将多个消费者划分到一个组中,监听同一个队列。
具体以下特点:
- 消息分流
- 队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度
- 消息标识
- 消费者组会维护一个标识,记录最后一个被处理的消息,哪怕消费者宕机重启,还会从标示之后读取消息。确保每一个消息都会被消费
- 消息确认
- 消费者获取消息后,消息处于pending状态,并存入一个pending-list。当消息处理完成后==需要通过XACK来确认消息==,标记消息为已处理,才会从pendingList移除。
xgroup create:创建消费者组
XGROUP CREATE key groupname ID|$ [MKSTREAM]]
xgroup create 队列名称 消费组名 起始消费id(0-0 或0表示从头开始消费,$表示从尾即最新消息开始消费)
- key: 队列名称
- groupName: 消费者组名称
- ID: 起始ID标示,$代表队列中最后一个消息,0则代表队列中第一个消息
- MKSTREAM: 队列不存在时自动创建队列
还存在其他命令,如下:
DEMO:
127.0.0.1:6379> XGROUP create stream2 group1 $ mkstream
OK
分组读取消息:xreadgroup
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key …] ID [ID …]
xreadgroup group 消费组名 消费者 (count n 可选参数,读取n条数据)(block 0 可选参数,阻塞等待毫秒数) streams 队列名称 消息id
-
group: 消费组名称
-
consumer: 消费者名称,如果消费者不存在,会自动创建一个消费者
-
count: 本次查询的最大数量
-
BLOCK milliseconds: 当没有消息时最长等待时间
-
NOACK: 无需手动ACK,获取到消息后自动确认**(不建议使用)**
-
STREAMS key: 指定队列名称
-
ID: 获取消息的起始ID: ——读取过的不再重复读,
设置具体消息id
:读取大于指定id的消息(不包含指定id的消息)'>'
:=从下一个未消费的消息开始(读取最新消息的意思)=其它
:根据指定id从pending-list中获取已消费但未确认的消息,例如0
,是从pending-list中的第一个消息开始(因为每次消费pending-list中的消息+确认ACK后,就会将该条消息从pending-list移除,所以0
每次都是队列中刷新后的第一天消息)
-
xreadgroup是写命令,只能在主服务器进行(它修改了游标等信息)。
DEMO:
可以看出,在消费者组内接收消息,不会重复消费,每次都是获取的下一条未处理的消息。
若设置ID 为其他时,如,设置ID=0。
两次获取pending-list中的同一条数据,因为未确认该消息,该消息一直在pending-list的第一条。我们将这条消息确认后可得到:
确认该条消息,再次执行XREADGROUP group group1 c1 count 1 block 10000 streams stream2 0
,可见已经获取的是第二条数据k2=v2了:
未确认消息:XPENDING
XPENDING key group [[IDLE min-idle-time] start end count [consumer]]
summary: Return information and entries from a stream consumer group pending entries list, that are messages fetched but never acknowledged.
- key: 队列名称
- group: 消费组名称
- [IDLE min-idle-time] : 消息被消费开始,到确认XACK之前的时间
- start : 需要获取未确认消息的起始下标,
-
:代表负无穷 - end : 需要获取未确认消息的末尾下标,
+
:代表正无穷 - count: 获取未确认消息的数量
- consumer: 指定消费者名称**(因为每个消费者消费过的消息,都存在自己的pending-list中)**
DEMO:
127.0.0.1:6379> XPENDING stream2 group1 - + 100
1) 1) "1692758490032-0" --消费ID
2) "c1" --消费者名称
3) (integer) 815246
4) (integer) 1
2) 1) "1692758493333-0"
2) "c1"
3) (integer) 811406
4) (integer) 1
3) 1) "1692758496586-0"
2) "c2"
3) (integer) 784393
4) (integer) 1
4) 1) "1692758499886-0"
2) "c2"
3) (integer) 782980
4) (integer) 1
5) 1) "1692758503581-0"
2) "c1"
3) (integer) 779702
4) (integer) 1
6) 1) "1692758508874-0"
2) "c2"
3) (integer) 776924
4) (integer) 1
可看出上述C1、C2消费者各自存在3条待确认的消息。
确认消息:xack
XACK key group ID [ID …]
xack 队列名称 消费组名 消息id…
已经读取但是未确认的消息会写入pending_list,确认XACK后会从pending_list删除,是为了确保消息至少被消费一次。
优点:
- 采用stream机数据结构,支持多生产、多消费
- 支持数据持久化(消息可回溯)
- 消息可阻塞读取(可设置阻塞时间)
- 没有消息漏读的风险
- 支持消息确认机制,保证消息最少被消费一次
Redis队列总结:
笔者在此仅以List、PubSub、Stream三种类型的消息队列进行对比,数据如下: