Transactional Outbox模式通过将消息写入outbox表并与业务数据共用同一数据库事务,确保变更与消息发布的原子性;再借助tx.AfterCommit钩子在提交后异步投递,避免事务回滚导致消息不一致或丢失。

事务内不能直接发消息,必须用 tx.AfterCommit
消息发件箱(Outbox Pattern)本质是把业务变更和消息发布原子化:更新数据库的同时,把待发送的消息写入一张 outbox_messages 表,再由独立消费者轮询或监听该表投递。GORM 本身不提供发件箱实现,但它的 tx.AfterCommit 钩子是唯一安全出口。
常见错误现象:在 db.Transaction 闭包里直接调 amqp.Publish() 或 rdb.Publish() —— 一旦事务回滚,消息已发出,造成状态不一致;若事务提交后网络失败,消息又丢失。
-
tx.AfterCommit只在事务成功提交后触发,且注册时必须在闭包内完成 - 钩子函数中不能再操作
tx(连接已释放),只能用新连接或外部客户端 - 如果钩子 panic,不影响数据库结果,但消息可能未发出,需自行加重试或落库兜底
outbox_messages 表结构与 GORM 模型定义
发件箱表不是辅助日志,而是核心一致性保障载体。字段设计要支持幂等消费和可靠投递:
type OutboxMessage struct {
ID uint `gorm:"primaryKey"`
Aggregate string `gorm:"index"` // 如 "order:123"
Type string `gorm:"index"` // 如 "OrderCreated"
Payload []byte `gorm:"type:json"`
SentAt *time.Time
CreatedAt time.Time
}
-
Aggregate+Type组合索引,方便按业务实体快速拉取未发送消息 -
Payload存 JSON 而非序列化字符串,避免反序列化歧义(如时间格式、空值处理) - 不要加
UNIQUE约束在Aggregate上——同一订单可能触发多条不同事件
事务中写发件箱 + 注册投递钩子的完整写法
关键点:所有 DB 操作(业务表 + outbox 表)必须走同一个 tx,且投递逻辑只注册不执行。
err := db.Transaction(func(tx *gorm.DB) error {
// 1. 更新业务数据
if err := tx.Create(&Order{UserID: 100, Amount: 299}).Error; err != nil {
return err
}
// 2. 写入发件箱(同事务)
msg := OutboxMessage{
Aggregate: "order:123",
Type: "OrderCreated",
Payload: []byte(`{"order_id":123,"user_id":100}`),
}
if err := tx.Create(&msg).Error; err != nil {
return err
}
// 3. 注册提交后投递(注意:此时不能用 tx)
tx.AfterCommit(func() {
// 这里用新连接或全局 MQ 客户端
if err := publishToMQ(msg); err != nil {
log.Warn("failed to publish outbox message", "id", msg.ID, "err", err)
// 可选:记录失败到 dead-letter 表,供补偿任务处理
}
})
return nil
})
- 切勿在
AfterCommit里再调tx.Create或任何带tx的方法——事务已结束 - 如果
publishToMQ是阻塞调用,考虑异步 goroutine + context 控制超时,避免拖慢事务响应 - 生产环境建议给
AfterCommit加简单重试(最多 1~2 次),避免瞬时网络抖动导致消息丢失
为什么不用 db.Begin() 手动控制?
手动事务容易漏掉 Commit() 或 Rollback(),更危险的是:在 defer tx.Rollback() 后忘记判断是否已提交,导致已提交的数据被二次回滚(MySQL 不允许对已提交事务调 Rollback,会 panic)。
db.Transaction 是唯一推荐方式,它自动处理三件事:panic 捕获、错误返回即回滚、nil 返回即提交。而发件箱模式恰恰依赖这种确定性 —— 你必须 100% 确认事务是否成功,才能决定是否触发消息投递。
最容易被忽略的一点:发件箱的可靠性不取决于 GORM,而取决于 AfterCommit 注册时机是否严格在事务闭包内、以及投递失败后的补偿机制是否完备。没有兜底的发件箱,比不用还危险。


















