本文是自己的学习笔记。主要参考资料如下:
马士兵
1、发布订阅
redis的发布系统是即发即忘的,也就是说,信息发送以后如果没有订阅者订阅这个频道,那这条消息就永久丢失了。
1.1、命令
-
发布:我们可以通过
publish channel1 msg1
发送信息,一次可对多个channel发送信息。
该命令的返回值是Integer
类型,表示订阅该channel的客户端数目。
-
订阅:我们可以通过
subscribe channel1
订阅频道,一次可订阅多个频道。
一旦开启订阅,线程会收到阻塞,只能等待收听信息,直到主动停止订阅。
返回的参数意义:"subscribe"
表示这是订阅操作;"topic1"
订阅的频道;1
表示订阅的第1个频道。
当其它客户端使用publish topic1 s1
发送信息后,收到信息后会返回以下信息
"message"
表示收到信息;"topic1"
表示信息来源于topic1
这个频道;"s1"
则是信息内容。
-
查看正在被监听的频道:
pubsub channels pattern
。pattern
是正则表达式,可以不选。
-
查看一频道的监听者数量:
pubsub numsub channel
。
2、stream
在5.0之后,redis参考kafka对发布订阅系统进行了升级。
消息发送后会存储在链表状的Stream中,这样信息就不会丢失。
2.1、stream的数据结构
Stream中信息的数据结构可以简单理解成Map<String, Map<String, String>>
。
第一个String
是信息的id,他的形式是时间戳-n
。系统生成的时间戳是毫秒级的,n
则是序号,表示这个信息是在这个毫秒内发送的第n条信息,比如1675003698412-0
表示在时间1675003698412
内的发送的第一条信息。
里面的Map<String, String>
是信息内容,它是Key->Value
的形式。
2.2、命令
2.2.1、生产者命令
- 发布信息:
xadd stream id key value
。stream
是stream
的名字,或者说channel
的名字。
id
则是信息的id,可以自定义,但不建议,因为redis自动生成的已经很好了。如果是交给系统生成,id
写成*
即可。比如xadd stream1 * k1 v1
。
key
和value
则是自定义的信息体,没什么限制,可以同时存储多个键值对。
命令的返回值是信息的id
。
127.0.0.1:6379> xadd stream * k1 v1
"1675003698412-0"
- 删除信息:
xdel stream id
。可一次删除多个。
127.0.0.1:6379> xdel stream 1675004972142-0
(integer) 1
- 获取信息:
xrange stream start end
。
start
和end
可以分别用-
和+
表示获取一个stream内的所有信息,比如xrange stream1 - +
。
但他们不能用数字表示,比如xrange stream1 0 1
永远都是返回空。
他们只能用id
来确定范围,比如xrange stream "1675004971053-0" +
获取id
为"1675004971053-0"
以及之后发布的信息。
127.0.0.1:6379> xrange stream "1675004971053-0" +
1) 1) "1675004971053-0"
2) 1) "k1"
2) "v1"
2) 1) "1675004971554-0"
2) 1) "k1"
2) "v1"
3) 1) "1675004972142-0"
2) 1) "k1"
2) "v1"
4) 1) "1675004972870-0"
2) 1) "k1"
2) "v1"
- 获取stream长度:
xlen stream
。
127.0.0.1:6379> xlen stream
(integer) 7
2.2.2、消费者命令
2.2.2.1、非阻塞消费
- 消费消息:
xread count num streams stream start
。
xread
和count
都是关键字。
num
是数字,表示这次读取信息的数目。
streams
也是关键字。
stream
是steam的名称。
start
是范围,格式是时间戳-n
,开区间
。就是说他的格式和id一样,时间戳数字表示,单位毫秒。后面的n
是当前时间下发送的第n条信息。一般我们可以写成0-0
表示从第一条信息开始读。
127.0.0.1:6379> xadd stream * k1 v1
"1675084877177-0"
127.0.0.1:6379> xadd stream * k2 v2
"1675084882732-0"
127.0.0.1:6379> xadd stream * k3 v3
"1675084888322-0"
127.0.0.1:6379> xread count 2 streams stream 0-0
1) 1) "stream"
2) 1) 1) "1675084877177-0"
2) 1) "k1"
2) "v1"
2) 1) "1675084882732-0"
2) 1) "k2"
2) "v2"
127.0.0.1:6379> xread count 2 streams stream 1675084882732-0
1) 1) "stream"
2) 1) 1) "1675084888322-0"
2) 1) "k3"
2) "v3"
2.2.2.2、阻塞式消费
xread block timeouts streams stream start
:
xread
和block
是关键字。要注意block
和count
不能一起使用。一起使用虽然不会报错,但是count
不能起作用,无论count
的值是多少,只要有一条数据可消费命令便结束并释放线程。
timeouts
是整数,单位是毫秒。如果从指定时间开始没有数据可消费,那就block线程timeouts
毫秒后释放。如果从start
时间开始有现成的数据,则读取start
后的所有数据,无需block;如果暂时没数据,在timeouts
之前有数据可消费,则读取数据且立即释放。
streams
关键字。
stream
消费的stream名称。
start
,格式是时间戳-n
,意义同上。这里可以使用$
表示现在,即阻塞消费最新数据,该命令之后没有生成新数据,阻塞timeouts
整数,阻塞timeouts毫秒,若中途有新数据可消费则释放线程。若要永久阻塞则设为0,
下面是例子。先插入两条数据,命令中start
设为在插入前,可以看到线程没有阻塞直接消费两条数据。
127.0.0.1:6379> xadd stream * k1 v1
"1675088119249-0"
127.0.0.1:6379> xadd stream * k2 v2
"1675088124903-0"
127.0.0.1:6379> xread block 1000 streams stream 0-0
1) 1) "stream"
2) 1) 1) "1675088119249-0"
2) 1) "k1"
2) "v1"
2) 1) "1675088124903-0"
2) 1) "k2"
2) "v2"
现在继续读取数据,但start
时间设为插入后,可以看到线程阻塞1s后释放。
# Note: start in command is 1675088124904 that is greater than 1675088124903
127.0.0.1:6379> xread block 1000 streams stream 1675088124904-0
(nil)
(1.04s)
先同上读取数据,timeouts
设为10s。在阻塞的10s内,在另一线程生成新的数据供其消费。可以看到阻塞中断并消费数据。
127.0.0.1:6379> xread block 10000 streams stream 1675088124904-0
1) 1) "stream"
2) 1) 1) "1675088389555-0"
2) 1) "k3"
2) "v3"
(3.45s)
# another thread
127.0.0.1:6379> xadd stream * k3 v3
"1675088389555-0"
现在使用$
代替start
,可以看到线程受到阻塞,直到10s后才释放。若我们在这10s内生成新数据,读取数据释放线程。
127.0.0.1:6379> xread block 10000 streams stream $
(nil)
(10.08s)
# produce data halfway
127.0.0.1:6379> xread block 20000 count 2 streams stream $
1) 1) "stream"
2) 1) 1) "1675088790630-0"
2) 1) "k4"
2) "v4"
(9.77s)
# another thread
127.0.0.1:6379> xadd stream * k4 v4
"1675088790630-0"
2.3、群组
上面介绍的消费都是单消费者,生产环境中多半是群组消费。
群组就是基于相同的业务逻辑,多个客户端组成的一个组。
消息队列的一大优点是流量削峰,生产者不和消费者强绑定,可以很快地生产大量消息,不必等消费者回应。
而消费者因为逻辑多半比较复杂,耗时较多,一个生产者生产的消息需要多个消费者一起工作才能及时处理。
比如生产者生产订单消息,消费者却要有短信通知,库存计算等逻辑。就短信通知来说,这个逻辑耗时较长,我们需要多个客户端(消费者)才能及时处理生产者的信息,那么这些这些客户端就应该组成一个群组,而处理库存计算的那一堆客户端又组成另一个群组。
2.3.1、数据结构
一个群组有两个主要属性:last_delivered_id
和Collection<Consumer>
last_delivered_id
是该群组消费的上一次消费的消息的id,那个格式为时间戳-n
的id。
Collection<Consumer>
则是属于这个群组的消费者集合。
Consumer
中有一个队列叫pending_ids
,消息从群组分发到指定的consumer
后会存放到这个队列中,里面存放着等待消费的消息id。
一个Consumer
对应一个客户端,客户端会从对应的Consumer
队列中拿消息继续消费。
2.3.1.1、确认消息
到这里我们明白,redis的消息最终是存放在consumer
的pending_ids
队列中等待对应的客户端消费。
客户端消费数据需要给redis一个回应,表示这个消息被我(客户端)完整消化了。收到确认消息后redis才放心把pending_ids
中对应的消息id删除。这保证每条消息都能被处理。
这个确认消息的指令是xack
,具体细节会在下面说。
2.3.2、命令
2.3.2.1、非消费指令
- 创建群组:
xgroup create stream groupName start
,
xgroup
和create
是关键字。
stream
是stream的名字,表示创建的群组会消费这个stream的消息。
groupName
,group的名字。
start
,格式是时间戳-n
,表示群组消费start以后的数据。这个start
也可以换成$
,表示只消费最新数据。
127.0.0.1:6379> xgroup create stream g1 0-0
OK
- 查询stream信息:
xinfo stream stream
,查询一个stream 的相关信息,长度,群组数等等。
xinfo
和stream
是关键字。
stream
是要查询的stream的名字。
127.0.0.1:6379> xinfo stream stream
1) "length" # stream的长度
2) (integer) 6
3) "radix-tree-keys"
4) (integer) 1
5) "radix-tree-nodes"
6) (integer) 2
7) "groups" # 该stream的group数量
8) (integer) 1
9) "last-generated-id"
10) "1675089527606-0" # 被最后消费的信息id,无论是但消费者消费的还是群组消费的,都会刷新这个值。
11) "first-entry" # stream第一个消息的信息
12) 1) "1675088119249-0"
2) 1) "k1"
2) "v1"
13) "last-entry" # stream最后一个消息的信息
14) 1) "1675089527606-0"
2) 1) "k5"
2) "v5"
- 查询stream上的群组信息:
xinfo groups stream
xinfo
和groups
是关键字。
stream
是要查询的stream的名字。
127.0.0.1:6379> xgroup create stream g1 0-0
OK
127.0.0.1:6379> xgroup create stream g2 $
OK
127.0.0.1:6379> xinfo groups stream
1) 1) "name"
2) "g1" # group的名字
3) "consumers"
4) (integer) 0
5) "pending"
6) (integer) 0
7) "last-delivered-id"
8) "0-0" # 因为g1的start是0-0,所以last-delivered-id是0-0
2) 1) "name"
2) "g2"
3) "consumers"
4) (integer) 0
5) "pending"
6) (integer) 0
7) "last-delivered-id"
8) "1675089527606-0" # 因为g2的start是$,所以last-delivered-id是stream的最后一个消息
- 查看群组上的
consumer
信息:xinfo consumers stream group
。
xinfo
和cosumers
是关键字。
stream
是stream的名字。
group
是group的名字。
下面的例子是查看stream
上的群组g1
上的所有consumer
。
127.0.0.1:6379> xinfo consumers stream g1
1) 1) "name"
2) "consumer1" # consumer的名字
3) "pending"
4) (integer) 3 # 还有3个消费没被对应客户端消费
5) "idle"
6) (integer) 2295789
2) 1) "name"
2) "consumer2"
3) "pending"
4) (integer) 1
5) "idle"
6) (integer) 24732
- 确认消息:
xack stream group id
xack
是关键字。
stream
是stream的名字。表示这次确认的消息来源于这个stream。
group
,group的名字,表示这次确认的消息由这个group处理。
id
,信息的id。命令执行后对应的group
上的所有consumer
的pending_ids
会删除这个id,防止同一个消息被重复处理。
下面的例子是确认消息,确认的消息来源于stream
,由群组g1
的consumer1
处理。确认消息后我们可以看到consumer1
的pending
由3变成2。
# 确认消息前
127.0.0.1:6379> xinfo consumers stream g1
1) 1) "name"
2) "consumer1"
3) "pending"
4) (integer) 3
5) "idle"
6) (integer) 2295789
2) 1) "name"
2) "consumer2"
3) "pending"
4) (integer) 1
5) "idle"
6) (integer) 24732
127.0.0.1:6379> xack stream g1 1675088119249-0
(integer) 1
# 确认消息后
127.0.0.1:6379> xinfo consumers stream g1
1) 1) "name"
2) "consumer1"
3) "pending"
4) (integer) 2
5) "idle"
6) (integer) 4090650
2) 1) "name"
2) "consumer2"
3) "pending"
4) (integer) 1
5) "idle"
6) (integer) 1819593
2.3.2.2、非阻塞消费
- 消费数据:
xreadgroup group groupName consumer count num streams stream >
xreadgroup
和group
是关键字。
groupName
,group的名字,数据将被这个group消费。
consumer
,consumer的名字,一个group
里有多个consumer
,所以需要确定消息被具体哪个consumer
消费。
count
,关键字。
num
,这次被消费的消息数量。
streams
,关键字。
stream
,stream的名字,表示消费这个stream的数据。
>
, 关键字。
下面是例子,意义是群组g1
的consumer1
从stream
中消费两条数据。
127.0.0.1:6379> xreadgroup group g1 consumer1 count 2 streams stream >
1) 1) "stream"
2) 1) 1) "1675088124903-0"
2) 1) "k2"
2) "v2"
2) 1) "1675088389555-0"
2) 1) "k3"
2) "v3"
2.3.2.3、阻塞式消费
-
阻塞式消费:
xreadgroup group groupName consumer block timeouts streams stream >
xreadgroup
和group
是关键字。
groupName
,group的名字,数据将被这个group消费。
consumer
,consumer的名字,一个group
里有多个consumer
,所以需要确定消息被具体哪个consumer
消费。
block
,关键字。要注意block
和count
不能一起使用。一起使用虽然不会报错,但是count
不能起作用,无论count
的值是多少,只要有一条数据可消费命令便结束并释放线程。
timeouts
,整数,单位是毫秒。表示如果没有数据可消费,将阻塞timeouts
毫秒。中途有数据可消费了则消费数据并释放线程。若值为0表示无限期阻塞。 要注意b
streams
,关键字。
stream
,stream的名字,表示消费这个stream的数据。
>
, 关键字。下面是例子,意义是群组
g2
的consumer1
从stream
中阻塞式消费数据,阻塞线程,10s后仍没有数据便释放线程。
127.0.0.1:6379> xgroup create stream g2 $
OK
127.0.0.1:6379> xreadgroup group g2 consumer1 block 10000 count 3 streams stream >
(nil)
(10.07s)
下面的例子意义是群组g2
的consumer1
从stream
中阻塞式消费数据,阻塞线程。之后有新数据可消费于是读取数据并释放线程。
127.0.0.1:6379> xreadgroup group g2 consumer1 block 0 streams stream >
1) 1) "stream"
2) 1) 1) "1675174395569-0"
2) 1) "k7"
2) "v7"
(20.71s)
# another thread
127.0.0.1:6379> xadd stream * k7 v7
"1675174395569-0"
下面可看到block
和count
一起使用时,count
不起作用。命令消费一个数据后便结束而不是消费三个数据才结束。
127.0.0.1:6379> xreadgroup group g2 consumer1 block 0 count 3 streams stream >
1) 1) "stream"
2) 1) 1) "1675175067281-0"
2) 1) "k9"
2) "v9"
(7.50s)
# another thread
127.0.0.1:6379> xadd stream * k9 v9
"1675175067281-0"
2.3.3、Stream or MQ
实际开发中我们应该选择Stream,还是选择独立的框架MQ或者kafka?
redis的主要功能还是缓存,只有当我们的资源有限,可能只有一台服务器,并且确定短时间内功能不会再拓展时,我们才会选择用Stream做消息队列。否则还是应该使用MQ,kafka等框架,他们的功能更强大,团队更专业。