还记得 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 干这活。
七、练手
XADD三条订单进orders,XRANGE orders - +能看到。XGROUP CREATE orders packer 0-0,两个窗口分别用node-a、node-b做XREADGROUP,确认两人拿到的 ID 不同。- 只 ACK 其中一条,
XPENDING里应该还剩一条。 - Redis 8.6 的话,用
IDMPAUTO把同一条支付记录发两遍,XLEN仍是 1。
能走完这四步,List 当年那几个坑基本都有着落了:消息还在、能签字、能换人接着干、重试不容易写重。
下期讲讲缓存的穿透、雪崩、击穿。
本次导航结束,欢迎 关注、点赞、转发。