选用Redis Pub/Sub而非NATS是因后者部署运维成本过高,而Redis已存在、延迟稳定、客户端成熟;但其不保证可达性与消费者组,仅适用于缓存失效等通知类场景。

为什么不用 NATS 而选 Redis Pub/Sub 做轻量总线
微服务间内部通信若不涉及严格有序、持久化或跨数据中心,NATS 的部署和运维成本远超必要。Redis Pub/Sub 在多数 Go 微服务场景中更实际:它已随基础设施部署、连接复用成熟、延迟稳定在 sub-millisecond 级,且 redis-go 客户端对 Publish/Subscribe 的封装足够干净。
但要注意:Redis Pub/Sub 不保证消息可达——断连期间发布的消息直接丢失;也不支持消费者组语义。仅适合「通知类」事件(如缓存失效、配置热刷、状态广播),不能替代 Kafka 或 RabbitMQ 处理订单、支付等关键链路。
- 用
redis-gov9+,避免老版本github.com/go-redis/redis的 context 传递陷阱 - 订阅必须用独立连接(
Conn),不能和命令连接混用,否则会阻塞写操作 - 频道名建议加服务前缀,例如
svc:auth:token_revoked,避免命名冲突
Go 中如何安全启动一个长期运行的订阅协程
常见错误是把 Subscribe 放在 main() 里直接调用,结果协程被主 goroutine 退出带崩,或没处理 ctx.Done() 导致资源泄漏。
正确做法是封装为可取消、可重连的订阅循环,且监听 ctx.Done() 主动关闭连接:
func runSubscriber(ctx context.Context, rdb *redis.Client, channel string) {
for {
pubsub := rdb.Subscribe(ctx, channel)
// 必须检查 err,网络闪断时 pubsub.Err() 可能非 nil
if err := pubsub.Err(); err != nil {
log.Printf("subscribe failed: %v", err)
time.Sleep(1 * time.Second)
continue
}
// 启动读取 goroutine,但需绑定 ctx 控制生命周期
go func() {
for {
select {
case msg :=
- 每次重连都新建
pubsub实例,不要复用旧实例 - 务必在
select分支中显式调用pubsub.Close(),否则底层连接不会释放 - 不要在
handleEvent中阻塞太久,建议投递到 worker pool,避免积压阻塞 channel 接收
发布端如何避免因 Redis 暂不可用导致业务逻辑失败
消息总线本质是「尽力而为」,发布失败不能让核心业务(如用户注册)失败。常见错误是同步调用 rdb.Publish(ctx, ch, payload).Err() 并 panic 或返回 error。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
应将发布降级为 fire-and-forget,并记录 warn 日志供事后排查:
func publishAsync(rdb *redis.Client, channel, payload string) {
// 使用短超时,避免阻塞业务流程
ctx, cancel := context.WithTimeout(context.Background(), 100 * time.Millisecond)
defer cancel()
if err := rdb.Publish(ctx, channel, payload).Err(); err != nil {
log.Warnf("publish to %s failed: %v", channel, err)
// 不 return error,不中断业务流
}
}
- 超时设为 100ms 是经验值:Redis 本地回环通常
- 不要用
context.TODO()或无限制 context,防止 goroutine 泄漏 - 如果业务强依赖消息送达(极少数情况),应在应用层加补偿机制,而非指望 Redis Pub/Sub
怎么验证消息确实被消费了,而不是静默丢弃
Redis Pub/Sub 没有确认机制,无法像 AMQP 那样 ack。验证只能靠「可观测性补位」:在消费端打结构化日志 + 暴露指标,再配合外部监控对齐时间窗口。
关键动作只有三件:
- 消费函数入口立刻打日志,含
msg.Channel、msg.Payload哈希、time.Now().UnixNano() - 用
prometheus.CounterVec统计各 channel 的接收数,label 包括service和channel - 在发布端也打日志和指标,用相同时间戳字段(如
trace_id)串联两端日志
真正容易被忽略的是时间精度:Redis Pub/Sub 不带时间戳,所有时间必须由发布方注入(例如在 payload 里加 "ts": 1717023456123),否则跨服务日志对齐误差可能达百毫秒级。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










