可靠投递靠「本地事务表 + 异步发送 + 幂等消费」三者咬合,而非 rabbitmq 的 channel.tx() 或 kafka 事务 api;channel.tx() 仅保证发到 broker,不保证消费成功且无补偿路径,吞吐低、逻辑缺陷明显。

直接说结论:可靠投递不是靠 RabbitMQ 的 channel.Tx() 或 Kafka 的事务 API,而是靠「本地事务表 + 异步发送 + 幂等消费」三者咬合。任何想绕过数据库写入、只靠 MQ 协议层兜底的方案,在生产环境都撑不过一次网络抖动。
为什么不能用 channel.Tx() 做事务消息
RabbitMQ 的 channel.Tx() 在 Go 里根本不能用——它不是性能问题,是逻辑缺陷:
-
channel.Tx()只保证“发到 Broker”,不保证消费者收到,更不防网络分区后消息静默丢失 - 实测吞吐掉到 200–300 msg/s,比 HTTP 轮询还慢
- 事务提交后,若下游服务宕机或处理失败,没有补偿路径,数据就永久不一致
别被“事务消息”这个词带偏:Go 语言本身没有这个原语,AMQP 协议也不提供跨服务一致性语义。
outbox_events 表结构和插入时机必须严格绑定业务事务
关键不是“有没有表”,而是“怎么写进去”。常见翻车点是把消息写入和业务操作拆成两个事务,或者用 goroutine 异步写:
- 必须用同一个
*sql.Tx插入业务记录(如orders)和消息记录(outbox_events),字段至少含:id、event_type、payload、processed(bool,默认 false) - 禁止在
defer tx.Commit()之后再写消息表——panic 时事务回滚,消息却已落库 - 禁止用
go func() { ... }()异步插入——进程崩溃时,goroutine 直接消失,消息永远不落地
正确姿势是在业务逻辑块内、tx.Commit() 前完成两条 INSERT,靠数据库原子性兜底。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
异步发送器必须带指数退避和状态更新
轮询 outbox_events 不是简单 SELECT ... WHERE processed = false 就完事:
- 每次最多取
LIMIT 100,避免长事务锁表;UPDATE 必须用WHERE id IN (…)精确匹配,防止并发重复处理 - 发送成功后,必须在同一事务中执行
UPDATE outbox_events SET processed = true WHERE id = ?,否则重试时会重复发 - 轮询间隔不能固定(比如每 100ms 查一次),要用指数退避:
time.Sleep(time.Second * 1)→*2→*4,避免 DB 被打爆 - 发送失败(如 MQ 连接超时、拒绝连接)时,不更新
processed,让下一轮继续尝试
注意:这里“发送失败”指网络层或协议层错误,不是业务逻辑失败——后者应由消费者幂等处理,不是生产端该管的事。
消费者幂等不能只靠 status 字段判重
Kafka 和 RabbitMQ 都不保证“恰好一次”,重试是常态。只写 UPDATE inventory SET stock = stock - 1 WHERE order_id = ? 是危险的:
- 必须先查唯一约束:
SELECT COUNT(*) FROM inventory_adjustments WHERE order_id = ? AND type = 'deduct' AND status = 'done',有结果就跳过 - 避免用
WHERE status != 'restored'这类非确定条件——可能漏掉已修复但状态未同步的记录 - 禁止用内存 map 或 Redis 缓存临时状态做判重——服务重启即失效,等于没做
- 所有 UPDATE 都要带业务条件 where,例如扣库存必须同时校验订单状态:
WHERE order_id = ? AND status = 'paid'
真正难的不是写代码,是把“消息发出”“消息送达”“业务执行”这三件事的状态全部持久化,并在每个环节断点可恢复。漏掉任意一环,系统就在一致性悬崖边上运行。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










