我不会起名字322 · 后端 / 算法 / 数据库
文章目录
- [Redis 从 0 到 1(十八):List、Pub/Sub、Stream 为什么生产只用 Stream?Go 实操](#Redis 从 0 到 1(十八):List、Pub/Sub、Stream 为什么生产只用 Stream?Go 实操)
-
- 一、两种"用起来挺好,出事就傻眼"的方案
- [二、Stream 核心模型(收藏点 1)](#二、Stream 核心模型(收藏点 1))
- [三、Go 实操:生产 + 消费组(收藏点 2)](#三、Go 实操:生产 + 消费组(收藏点 2))
- [四、生产检查清单(收藏点 3)](#四、生产检查清单(收藏点 3))
- 五、命令清单
- 六、排查步骤:消息丢了从哪查
- 七、总结
Redis 从 0 到 1(十八):List、Pub/Sub、Stream 为什么生产只用 Stream?Go 实操
上一篇《Redis 从 0 到 1(十七):订单 30 分钟不支付自动关闭------ZSet 延迟队列 Go 实现》我们实现了定时任务,链接:https://blog.csdn.net/2501_92769340/article/details/167399800 。
本篇处理"实时消息":服务间异步通知、事件分发。Redis 有 3 种消息方案,生产环境的选择出奇地一致。
一、两种"用起来挺好,出事就傻眼"的方案
List 当队列 (第 3 篇演示过):LPUSH 生产、BRPOP 阻塞消费。演示完美------消费进程崩了呢?BRPOP 把消息弹出给客户端后,消息就在客户端内存里,进程一崩,消息凭空蒸发,没有任何痕迹。
Pub/Sub :PUBLISH 广播给所有订阅者,实时性极好。但它是"fire and forget":订阅者断线期间的消息直接丢弃,Redis 不为任何人保存消息。发布者也不知道到底有没有人收到。
| 特性 | List 队列 | Pub/Sub | Stream |
|---|---|---|---|
| 消息持久化 | 消费即删(崩溃丢) | 无 | 有(按 ID 追加,独立保留) |
| 断线续传 | 不可能 | 不可能 | 消费组 + pending 列表可以 |
| 多消费者负载均衡 | 不行(抢消息语义) | 不行(广播语义) | 消费组内竞争消费 |
| 消息确认 | 无 | 无 | XACK 显式确认 |
Stream(Redis 5.0+)把"消息"和"消费进度"分开存储:消息按 ID 永久追加在 stream 里(可设 MAXLEN 封顶),消费组各自记录自己读到哪、哪些没 ACK。
二、Stream 核心模型(收藏点 1)
XADD stream:orders * order_id 1001 action create
→ 追加一条消息,* 表示由 Redis 生成毫秒时间戳+序号的全局递增 ID
→ 消息本体存在 stream 里,与任何消费者无关
XGROUP CREATE stream:orders g1 0
→ 创建消费组 g1,从 ID 0(队首)开始读
→ 消费组的状态(last_delivered_id、pending)独立保存
XREADGROUP GROUP g1 c1 COUNT 10 STREAMS stream:orders >
→ 消费者 c1(属组 g1)读 10 条**新**消息(> 表示组里没人读过的)
→ 读到的消息进入 c1 的 pending 列表(已投递未确认)
→ 同一组内其他消费者再读 > 不会拿到同一条(组内竞争)
XACK stream:orders g1 <id> → 处理完确认,从 pending 移除
XPENDING ... → 看 pending 列表(谁欠了多少条)
XAUTOCLAIM ... → 把超时未 ACK 的消息转给其他消费者(故障转移)
可靠性语义:至少一次投递。消息进 pending 后只有 XACK 才删除,处理失败/消费者崩溃的消息靠 XAUTOCLAIM 被别的实例接管------这正是 List 和 Pub/Sub 永远给不了的。
三、Go 实操:生产 + 消费组(收藏点 2)
go
package main
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
"redis-0to1/redisx"
)
func main() {
rdb, err := redisx.NewClient(redisx.Config{Addr: "127.0.0.1:6379", Password: "redis123", PoolSize: 10})
if err != nil {
panic(err)
}
ctx := context.Background()
stream, group := "stream:orders", "order-workers"
// 消费组幂等创建(已存在会报错,忽略即可)
_ = rdb.XGroupCreate(ctx, stream, group, "0").Err()
// 1. 生产者:XADD,* 自动生成 ID
for i := 1; i <= 5; i++ {
id, err := rdb.XAdd(ctx, &redis.XAddArgs{
Stream: stream,
MaxLen: 100000, // 封顶防内存膨胀
Values: map[string]any{"order_id": 1000 + i, "action": "create"},
}).Result()
if err != nil {
panic(err)
}
fmt.Println("已生产:", id)
}
// 2. 消费者:组内竞争消费 + ACK
for {
msgs, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: group,
Consumer: "consumer-1",
Streams: []string{stream, ">"}, // > = 只读新消息
Count: 10,
Block: 2 * time.Second, // 没有新消息阻塞 2 秒
}).Result()
if err != nil {
if err.Error() == "redis: nil" { // 超时无消息
continue
}
panic(err)
}
for _, m := range msgs {
for _, msg := range m.Messages {
fmt.Println("消费:", msg.ID, msg.Values)
// 业务处理(幂等:order_id 去重)
if err := rdb.XAck(ctx, stream, group, msg.ID).Err(); err != nil {
fmt.Println("ACK 失败(将留在 pending,可被接管):", err)
}
}
}
break // 演示:消费一轮就退出
}
// 3. 运维三件套:积压、认领、封顶
pend, _ := rdb.XPending(ctx, stream, group).Result()
fmt.Println("组内 pending 概况:", pend)
// XAutoClaim:把 idle 超过 60s 的消息转给消费者 consumer-2
// rdb.XAutoClaim(ctx, &redis.XAutoClaimArgs{...})
}
四、生产检查清单(收藏点 3)
- MaxLen 必须设 :Stream 只增不删,不设上限就是内存泄漏;
XADD MAXLEN ~ 100000的~表示近似裁剪(不阻塞主线程),第 22 篇会解释为什么精确裁剪危险; - 消费必须 ACK + 幂等:至少一次投递意味着重复消息必然存在(ACK 后崩溃重投),业务用唯一键/状态机去重;
- pending 监控告警 :
XPENDING的 pending 数持续上涨 = 有消费者处理失败或崩溃,配合 XAUTOCLAIM 自动接管; - 消费者命名 :consumer 名带实例标识(
consumer-1@pod-3),排查 pending 时一眼定位; - 别用 XREAD 代替消费组:XREAD 没有 ACK/pending 语义,只是把 List 的问题换了个壳。
五、命令清单
| 命令 | 作用 |
|---|---|
XADD key [MAXLEN ~ n] * f v |
生产消息 |
XGROUP CREATE key g id |
建消费组(id=0 从头读,$ 只读新) |
XREADGROUP GROUP g c COUNT n BLOCK ms STREAMS key > |
组内消费 |
XACK / XPENDING / XAUTOCLAIM |
确认 / 查积压 / 故障接管 |
XLEN / XTRIM key MAXLEN ~ n |
长度 / 手动裁剪 |
XINFO STREAM key |
看流状态(首末 ID、消费组列表) |
六、排查步骤:消息丢了从哪查
- 先分责任:生产没发出(业务日志)还是消费没确认(pending 列表);
XINFO STREAM看消息总量和分组,定位消息卡在哪个组;XPENDING key group按消费者列出 pending------卡死的消息 idle 时间会暴露崩溃的消费者;XAUTOCLAIM接管后检查业务幂等性,确认不是重复消费问题。
七、总结
- List 崩溃丢消息、Pub/Sub 断线丢消息,两者都不能承担生产消息语义;
- Stream = 持久化消息 + 消费组进度 + ACK/pending,实现至少一次投递;
- 至少一次 = 必然重复,业务幂等是入场券;
- MaxLen 封顶 + pending 监控,两个必须配齐。
下一篇预告:消息链路打通了,下一篇回到每个系统都有的需求------登录态。下一篇《JWT 还是 Redis Session?登录态存储的 Go 实现》,我们把"踢人下线、滑动续期、多端登录"一次讲清。