分布式事务在Go微服务中不能直接用database/sql的Begin/Commit,因其仅作用于单个数据库连接,而微服务跨进程、跨DB实例,本地事务失效;Saga模式通过拆分为带补偿的本地事务链实现最终一致性。

分布式事务在 Go 微服务中为什么不能直接用 database/sql 的 Begin/Commit
因为 Begin/Commit 只作用于单个数据库连接,而微服务天然跨进程、跨数据库实例。你调用用户服务扣减余额,再调用订单服务创建订单,这两个操作分布在不同服务、不同 DB,本地事务完全失效。强行封装成“一个事务”只会让系统在失败时处于中间态——比如余额已扣但订单没建,或者反过来。
Saga 模式是 Go 微服务中最可行的数据一致性落地方式
Saga 把一个分布式业务流程拆成一系列本地事务,每个步骤都有对应的补偿操作。Go 生态里没有开箱即用的 Saga 框架,但可以用轻量组合实现:
- 用
github.com/celrenheit/slog或原生log/slog记录每步执行状态(含tx_id、step、status) - 补偿逻辑写成幂等函数,例如
RefundBalance(ctx, userID, amount)必须先查refund_log表判断是否已执行 - 用 Redis 的
SETNX+ 过期时间做分布式锁,防止同一笔事务被重复回滚 -
异步任务用
github.com/hibiken/asynq推送补偿任务,避免阻塞主流程
func CreateOrderSaga(ctx context.Context, req *CreateOrderRequest) error {
txID := uuid.New().String()
if err := debitBalance(ctx, txID, req.UserID, req.Total); err != nil {
return err
}
if err := createOrder(ctx, txID, req); err != nil {
// 触发补偿:退款
asynqClient.EnqueueContext(ctx, asynq.NewTask("compensate:debit", map[string]interface{}{
"tx_id": txID,
"user_id": req.UserID,
"amount": req.Total,
}))
return err
}
return nil
}
最终一致性场景下,避免轮询,改用事件驱动 + 状态机
当业务允许短暂不一致(如库存预占后异步扣减),不要在订单服务里循环查库存服务接口。正确做法是:
- 库存服务完成预占后,发
InventoryReservedEvent到 Kafka 或 NATS - 订单服务订阅该事件,用
github.com/ThreeDotsLabs/watermill处理,更新本地order.status = "reserved" - 状态变更走显式状态机(推荐
github.com/looplab/fsm),禁止直接UPDATE order SET status = 'paid'绕过校验 - 超时未支付?由独立的
timeout-checker服务监听order.created_at,触发ReleaseInventory事件
本地消息表 + 事务性发件箱是 Go 中最稳的可靠事件投递方案
很多团队用“先发消息再更新 DB”或“先更新 DB 再发消息”,两者都可能丢事件。真正可靠的方案是把事件写入和业务更新放在同一个本地事务里:
mallcloud商城基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba并采用前后端分离vue的企业级微服务敏捷开发系统架构。并引入组件化的思想实现高内聚低耦合,项目代码简洁注释丰富上手容易,适合学习和企业中使用。真正实现了基于RBAC、jwt和oauth2的无状态统一权限认证的解决方案,面向互联网设计同时适合B端和C端用户,支持CI/CD多环境部署,并提
立即学习“go语言免费学习笔记(深入)”;
- 在订单库中建
outbox_events表,字段含payload TEXT、topic VARCHAR、processed BOOLEAN DEFAULT false - 在
CreateOrder的 DB 事务内,用同一*sql.Tx插入订单记录 + 插入 outbox 记录 - 后台 goroutine 定期扫描
outbox_events WHERE processed = false,成功投递后更新processed = true - 注意:扫描间隔建议设为 100ms~500ms,太短压 DB,太长延迟高;用
SELECT ... FOR UPDATE SKIP LOCKED避免多实例重复处理
这个模式不依赖外部消息队列事务支持,也不要求 Kafka 开启事务(Go 的 segmentio/kafka-go 对事务支持有限且复杂),适合大多数中小规模 Go 微服务。
真正的难点不在代码怎么写,而在如何定义每个服务的“事务边界”和“补偿粒度”——比如退款是按订单退,还是按子项退?这直接影响状态表设计和补偿逻辑复杂度。没想清楚就写 Saga,最后会变成一堆难以调试的补偿嵌套。









