Redis Streams——有确认、能回溯的消息队列

还记得 List 做队列吗?BRPOP 一把把消息拿走,消费者这时候挂了,这条就没了。Streams 不这么干。

本次导航

  • Stream 长什么样:每条消息一个 ID,后面跟一堆字段
  • 读写:XADD、XRANGE、XREAD
  • 消费者组:XREADGROUP + XACK,挂了还能捞回来
  • Redis 8.6 的幂等投递:IDMPAUTO / IDMP
  • 和 Kafka 比,什么时候用 Redis 就够了

发车前提醒:Redis 核心数据结构(二)------List 与消息队列 这篇文章已经把 List 当队列的坑写过了,这篇接着往下做。

先用 docker 启动容器,还是之前的容器:

bash 复制代码
docker exec -it redis-demo redis-cli

一、Stream 是一条只往后追加的日志

别把它想成 List,List 的元素被 POP 之后就从 Key 里消失了;Stream 更像一本流水账,新消息往后面贴,旧的还在,除非你主动裁。

每条消息两个部分:

  • ID :默认是 毫秒时间戳-序号,比如 1789086136869-0。你那边数字肯定不一样,规则一样。
  • 字段 :就是一堆 field-value,跟 Hash 挺像。一条订单可以带 user_id、amount、sku。
bash 复制代码
127.0.0.1:6379> XADD orders * user_id 1001 amount 299 sku sku_1001
"1789086136869-0"

127.0.0.1:6379> XADD orders * user_id 1002 amount 88 sku sku_2002
"1789086136938-0"

127.0.0.1:6379> XLEN orders
(integer) 2

* 的意思是:ID 让 Redis 自己生成。一般别手写 ID,时钟回拨、多实例抢号都是麻烦。

二、先当普通日志读

按范围翻:

bash 复制代码
127.0.0.1:6379> XRANGE orders - +
1) 1) "1789086136869-0"
   2) 1) "user_id"
      2) "1001"
      3) "amount"
      4) "299"
      5) "sku"
      6) "sku_1001"
2) 1) "1789086136938-0"
   2) 1) "user_id"
      2) "1002"
      3) "amount"
      4) "88"
      5) "sku"
      6) "sku_2002"

- 是最小,+ 是最大。只看某一段就写成 XRANGE orders 1789086136869-0 1789086136869-0。

从某个 ID 之后接着读,用 XREAD。0-0 表示从最早开始;$ 表示只看比现在更新的:

bash 复制代码
127.0.0.1:6379> XREAD COUNT 1 STREAMS orders 0-0
1) 1) "orders"
   2) 1) 1) "1789086136869-0"
         2) 1) "user_id"
            2) "1001"
            ...

想卡着等新消息,加上 BLOCK,单位毫秒,0 表示一直等。空流的时候它会挂起,有人 XADD 才醒,这点和 BRPOP 类似:

bash 复制代码
XREAD BLOCK 10000 STREAMS orders $

Stream 可以在写入时裁一刀:

bash 复制代码
XADD orders MAXLEN 1000 * user_id 1003 amount 50 sku sku_3003

大概只留最近 1000 条。精确裁用 MAXLEN,量大时可以写成 MAXLEN ~ 1000,允许 Redis 少裁几条,换更快的速度。老数据要归档就 XTRIM,或者定时把 XRANGE 扫出来丢到别的地方。

三、消费者组:同一条消息别抢两遍

XREAD 是广播味道的------每个客户端自己记游标,两个进程都能读到同一条。做任务队列通常不是这个需求。你要的是:一组工人抢活,一条订单只被一个人处理。

这就是消费者组。

bash 复制代码
# 从头开始读历史。只想收新消息,把 0-0 换成 $
127.0.0.1:6379> XGROUP CREATE orders packer 0-0
OK

$ 和 0-0 搞反是新手常踩的坑。组已经建了、流还不存在时,后面加 MKSTREAM,让 Redis 顺手建一个空流。

两个工人分别叫 node-a、node-b:

bash 复制代码
127.0.0.1:6379> XREADGROUP GROUP packer node-a COUNT 1 STREAMS orders >
1) 1) "orders"
   2) 1) 1) "1789086136869-0"
         2) ... user_id 1001 ...

127.0.0.1:6379> XREADGROUP GROUP packer node-b COUNT 1 STREAMS orders >
1) 1) "orders"
   2) 1) 1) "1789086136938-0"
         2) ... user_id 1002 ...

> 表示:给我一条组里还没分过的新消息。node-a 拿走第一单,node-b 拿走第二单,不会撞车。

消息被读走之后,还在 Stream 里。Redis 另外记了一本账:谁领了、还没签字。这本账叫 PEL(Pending Entries List)。

bash 复制代码
127.0.0.1:6379> XPENDING orders packer
1) (integer) 2
2) "1789086136869-0"
3) "1789086136938-0"
4) 1) 1) "node-a"
      2) "1"
   2) 1) "node-b"
      2) "1"

处理完了,签个字:

bash 复制代码
127.0.0.1:6379> XACK orders packer 1789086136869-0
(integer) 1

XACK 只是从 PEL 里划掉,不是删消息 。别的组还能再读同一条;你自己以后用 XRANGE 也能翻到。这就是 List 做不到的回溯。

再建一个组试试:

bash 复制代码
127.0.0.1:6379> XGROUP CREATE orders stock 0-0
OK
127.0.0.1:6379> XREADGROUP GROUP stock warehouse-1 COUNT 1 STREAMS orders >
# 又拿到 1001 那单

packer 负责打包,stock 负责扣库存,各记各的进度。组与组之间互不打扰。

四、工人挂了,活怎么转出去

node-a 领了单,还没 XACK 就进程没了。

消息不会丢,它还挂在 PEL 里。

别人可以用 XPENDING 看到,再用 XAUTOCLAIM 认领超时的单:

bash 复制代码
# 把 packer 组里 idle 超过 60000 毫秒的未确认消息,转给 node-b
XAUTOCLAIM orders packer node-b 60000 0-0 COUNT 10

XCLAIM 是指定 ID 硬抢,XAUTOCLAIM(6.2+)更省事,按空闲时间批量捞。线上常见写法:工人循环 XREADGROUP 拿新活,隔一会儿再 XAUTOCLAIM 扫一遍别人留下的残骸。

还没 ACK 的时候,同一个工人把 ID 写成 0 再读一次,读到的是自己 PEL 里的旧单,不是新单。重启之后先用这个把上次没做完的活做掉,再去抢 >。

五、生产者重试,别写出两条一样的

Redis 8.6 给 XADD 加了幂等。

网络抖一下,客户端重发,以前会在流里留下两条一模一样的订单。

完整写法要带生产者 ID ,ID 仍然用 *:

bash 复制代码
127.0.0.1:6379> XADD pay_log IDMPAUTO pay-svc-1 * order_id 9001 amount 299
"1789086143272-0"

# 内容一样,再发一次,ID 还是这条,流的长度不变
127.0.0.1:6379> XADD pay_log IDMPAUTO pay-svc-1 * order_id 9001 amount 299
"1789086143272-0"

127.0.0.1:6379> XLEN pay_log
(integer) 1

IDMPAUTO 按消息内容算一个内部指纹,内容相同就当重试,服务重启后,同一个进程要继续用同一个生产者 ID(上面的 pay-svc-1),换一个就失效了。

内容可能真的重复------比如用户连点两下,两笔金额一样------就别用 IDMPAUTO,自己带业务单号:

bash 复制代码
XADD pay_log IDMP pay-svc-1 txn-9001 * order_id 9001 amount 299
XADD pay_log IDMP pay-svc-1 txn-9001 * order_id 9001 amount 299   # 重试,还是同一条
XADD pay_log IDMP pay-svc-1 txn-9002 * order_id 9001 amount 299   # 另一笔,会新增

幂等记录默认只记大约 100 秒、每个生产者最多 100 个 ID。隔太久再重试,Redis 可能已经忘了,又会写入一条。业务上重试窗口比较长的话,用 XCFGSET 调大:

bash 复制代码
XCFGSET pay_log IDMP-DURATION 600 IDMP-MAXSIZE 1000

注意:XCFGSET 会清掉当前这条流上已经记下的幂等指纹,改完配置再跑你的重试逻辑。

六、和 Kafka 比

Kafka 分区、副本、消费积压、跨机房,这些不是 Redis Streams 要做的事。Streams 吃的是 Redis 已经在那、消息量不大、丢了会心疼但还没到要单独养一套 Kafka 的场景:下单后发短信、扣积分、刷新库存、服务内部的任务队列。

几个硬差别:

  • 数据在 Redis 内存里,堆积能力看你给 Redis 的内存和 MAXLEN,不是磁盘日志
  • 高可用靠 Redis 自己的复制 / 集群,不是 Kafka 那套 ISR
  • 消费者组够用,管理命令也少,没有 Broker、Topic、再均衡那一长串运维

如果允许偶尔丢、逻辑又简单,List 就够了。

要确认、要重放、不要消息重复,用 Streams。

真要日千万级、还得按分区扩,去看 Kafka 或 RocketMQ,不要逼 Redis 干这活。

七、练手

  1. XADD 三条订单进 orders,XRANGE orders - + 能看到。
  2. XGROUP CREATE orders packer 0-0,两个窗口分别用 node-a、node-b 做 XREADGROUP,确认两人拿到的 ID 不同。
  3. 只 ACK 其中一条,XPENDING 里应该还剩一条。
  4. Redis 8.6 的话,用 IDMPAUTO 把同一条支付记录发两遍,XLEN 仍是 1。

能走完这四步,List 当年那几个坑基本都有着落了:消息还在、能签字、能换人接着干、重试不容易写重。

下期讲讲缓存的穿透、雪崩、击穿。


本次导航结束,欢迎 关注、点赞、转发。

相关推荐
小马同学-2 小时前
Redis消息队列与客户端编程
数据库·redis
fengkai45453 小时前
十二、Redis -2
运维·数据库·redis·容器
hweiyu003 小时前
Redis命令:BLPOP
redis·缓存
敲个大西瓜3 小时前
langchain笔记(一)
redis·笔记·langchain
小小龙学IT8 小时前
C++ Redis 客户端深度解析(hiredis 与 redis-plus-plus)——工业数采实时缓存 C++ 侧实战
c++·redis·缓存
mengge.cloud8 小时前
Redis
运维·服务器·数据库·redis·缓存·云计算
欣栀♡8 小时前
Redis 缓存穿透、击穿、雪崩,背出定义只是及格线
数据库·redis·缓存
codigger9 小时前
Redis 正式接入 AI:当"最懂速度的数据库"开始解决"记忆问题"
redis·分布式·后端·ai·向量检索
禾小西9 小时前
08丨Redis 哨兵集群:哨兵故障后还能完成切换吗?
数据库·redis·缓存