Redis学习笔记:订阅发布,Stream

本文深入探讨Redis Stream特性,包括发布订阅机制、Stream数据结构及命令详解,并对比传统消息队列,帮助读者理解Stream如何高效处理消息。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文是自己的学习笔记。主要参考资料如下:
马士兵

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 patternpattern是正则表达式,可以不选。
    请添加图片描述

  • 查看一频道的监听者数量: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 valuestreamstream的名字,或者说channel的名字。
    id则是信息的id,可以自定义,但不建议,因为redis自动生成的已经很好了。如果是交给系统生成,id写成*即可。比如xadd stream1 * k1 v1
    keyvalue则是自定义的信息体,没什么限制,可以同时存储多个键值对。
    命令的返回值是信息的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
    startend可以分别用-+表示获取一个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
    xreadcount都是关键字。
    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
    xreadblock是关键字。要注意blockcount不能一起使用。一起使用虽然不会报错,但是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_idCollection<Consumer>

last_delivered_id是该群组消费的上一次消费的消息的id,那个格式为时间戳-n的id。

Collection<Consumer>则是属于这个群组的消费者集合。

Consumer中有一个队列叫pending_ids,消息从群组分发到指定的consumer后会存放到这个队列中,里面存放着等待消费的消息id。

一个Consumer对应一个客户端,客户端会从对应的Consumer队列中拿消息继续消费。
在这里插入图片描述

2.3.1.1、确认消息

到这里我们明白,redis的消息最终是存放在consumerpending_ids队列中等待对应的客户端消费。

客户端消费数据需要给redis一个回应,表示这个消息被我(客户端)完整消化了。收到确认消息后redis才放心把pending_ids中对应的消息id删除。这保证每条消息都能被处理。

这个确认消息的指令是xack,具体细节会在下面说。


2.3.2、命令

2.3.2.1、非消费指令
  • 创建群组:xgroup create stream groupName start
    xgroupcreate是关键字。
    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 的相关信息,长度,群组数等等。
    xinfostream是关键字。
    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
    xinfogroups是关键字。
    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
    xinfocosumers是关键字。
    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上的所有consumerpending_ids会删除这个id,防止同一个消息被重复处理。
    下面的例子是确认消息,确认的消息来源于stream,由群组g1consumer1处理。确认消息后我们可以看到consumer1pending由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 >
    xreadgroupgroup是关键字。
    groupName,group的名字,数据将被这个group消费。
    consumer,consumer的名字,一个group里有多个consumer,所以需要确定消息被具体哪个consumer消费。
    count,关键字。
    num,这次被消费的消息数量。
    streams,关键字。
    stream,stream的名字,表示消费这个stream的数据。
    >, 关键字。
    下面是例子,意义是群组g1consumer1stream中消费两条数据。
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 >
    xreadgroupgroup是关键字。
    groupName,group的名字,数据将被这个group消费。
    consumer,consumer的名字,一个group里有多个consumer,所以需要确定消息被具体哪个consumer消费。
    block,关键字。要注意blockcount不能一起使用。一起使用虽然不会报错,但是count不能起作用,无论count的值是多少,只要有一条数据可消费命令便结束并释放线程。
    timeouts,整数,单位是毫秒。表示如果没有数据可消费,将阻塞timeouts毫秒。中途有数据可消费了则消费数据并释放线程。若值为0表示无限期阻塞。 要注意b
    streams,关键字。
    stream,stream的名字,表示消费这个stream的数据。
    >, 关键字。

    下面是例子,意义是群组g2consumer1stream中阻塞式消费数据,阻塞线程,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)

下面的例子意义是群组g2consumer1stream中阻塞式消费数据,阻塞线程。之后有新数据可消费于是读取数据并释放线程。

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"

下面可看到blockcount一起使用时,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等框架,他们的功能更强大,团队更专业。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值