真解耦必须用消息队列,RabbitMQ是Go微服务最可控选择;HTTP/gRPC handler内禁止直连RabbitMQ,须通过缓冲chan解耦并由独立goroutine调publishToRabbitMQ统一处理重试与重建。

直接用 goroutine 调 HTTP 或 gRPC 不算异步解耦,只是把同步调用藏进后台协程——下游一挂,协程就堆积,消息就丢失,服务照样级联雪崩。真解耦必须靠消息队列,而 RabbitMQ 是 Go 微服务里最可控、最易落地的选择。
为什么不能在 HTTP/gRPC handler 里直接 ch.Publish
HTTP 响应一旦写出(w.WriteHeader 或 w.Write),底层连接可能被复用或关闭。此时再调 ch.Publish,轻则 panic,重则消息静默丢失,且无日志可查。
- handler 只做两件事:序列化消息体、写入一个带缓冲的
chan []byte(如make(chan []byte, 1000)) - 用独立 goroutine 持续消费该 channel,并在封装函数
publishToRabbitMQ()中统一处理重试、连接重建、失败落库 - 绝对禁止在 Gin/echo 的中间件或 handler 内直接调
amqp.Connection.Channel()或ch.Publish()
amqp.Connection 必须全局单例且带心跳与超时
每次 amqp.Dial 都新建 TCP 连接,高并发下很快耗尽本地端口,报 connect: cannot assign requested address;DNS 解析失败时还默认无限阻塞。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 用
sync.Once初始化全局*amqp.Connection,URL 中必须含connect_timeout=5和heartbeat=30 - 监听
conn.NotifyClose,触发后清空旧连接,用指数退避(1s → 2s → 4s)重建 -
Connection复用,Channel按需创建、用完即ch.Close();Channel 泄漏会导致 RabbitMQ 报channel error: too many channels
发消息不设 DeliveryMode=Persistent 就等于没发
默认 DeliveryMode: amqp.Transient 意味着消息只存在内存,Broker 重启或断电,全丢。这不是异步,是“假装发了”。
- 声明队列时必须传
durable: true,否则DeliveryMode: amqp.Persistent无效 -
amqp.Publishing中必须设DeliveryMode: amqp.Persistent和mandatory: true -
mandatory: true让路由失败(如 exchange 不存在、binding 缺失)时立即返回 error,而不是静默丢弃
消费者不手动 defer msg.Ack 就是给自己埋雷
RabbitMQ 的 consumer 不是“收到就干”,而是“收到→干活→显式 Ack→再收下一条”。很多人在 handler 里把 msg.Ack(false) 写在业务逻辑中间,一旦前面 panic、return 或数据库报错,Ack 就被跳过——消息被 RabbitMQ 一直 hold 住,最终积压、触发流控、拖垮整个 channel。
- consumer handler 开头第一行就写
defer msg.Ack(false),确保无论怎么退出都执行 - 业务逻辑必须幂等:消息可能重复投递(网络分区、Ack 超时未达 Broker),得靠
event_id或业务单号去重,别信“RabbitMQ 只发一次” - 若业务失败需重试,用
msg.Nack(requeue: true)并配死信队列(DLX)+ TTL 控制重试次数,别无限重试
最容易被忽略的是幂等性设计和连接生命周期管理——它们不出问题时一切正常,一出就是线上事故。Connection 和 Channel 的复用策略、Ack 的位置、DeliveryMode 与 durable 的配合,这三处细节不抠清楚,RabbitMQ 就只是个更花哨的 log.Println。

















