gorm不支持分布式事务,仅保障单库acid;跨服务需saga+本地消息表,业务与消息同事务写入、定时投递加锁、补偿前查状态防超卖。

GORM 本身不支持分布式事务,它只管单数据库的 ACID;跨服务调用必须靠外部协调机制,不是加个 db.Transaction() 就能解决。
为什么 db.Transaction() 在微服务里会失效
很多人在订单服务里写 db.Transaction(),然后调用库存服务 HTTP 接口,以为“包住就原子”。实际是:本地事务提交了,远程调用失败或超时,两边状态立刻撕裂。
-
db.Transaction()只对当前*gorm.DB实例生效,无法感知、控制其他服务的数据库连接 - HTTP 调用没有事务上下文透传,
XID头没被下游解析,TC(Transaction Coordinator)根本收不到分支注册请求 - 哪怕用了
dtmcli.SagaNew或dapr.SendEvent,如果补偿逻辑没查状态就硬执行(比如直接UPDATE inventory SET stock = stock + ?),会超卖或资金错乱
微服务中结合 GORM 的可行路径:Saga + 本地消息表
这是目前 Go 生产环境最可控的组合。GORM 负责本地事务和消息表写入,Saga 协调器(如 DTM 或 Dapr)负责步骤编排和失败重试。
- 业务逻辑和消息记录必须在同一个
db.Transaction()中完成:INSERT INTO orders和INSERT INTO outbox (content, status)同库同事务 - 投递任务不能用 goroutine 异步发,要由定时任务扫描
outbox表,且加 Redis 分布式锁(SET lock:outbox:dispatch NX EX 30)防重复 - GORM 查询消息时,用
SELECT ... FOR UPDATE配合WHERE status = 'pending',避免并发消费 - 补偿操作必须带前置条件更新,例如:
UPDATE orders SET status = 'canceled' WHERE id = ? AND status = 'pending',防止重复取消
Dapr 场景下 GORM 如何配合 Saga 构建块
如果你选 Dapr,GORM 不参与协调,只做本地数据落地。关键点在于 HTTP 请求体结构和幂等控制。
- 每个子步骤的 HTTP POST body 必须含
id和data字段,Dapr 才能自动重试;GORM 写入时用该id作为业务主键或关联字段 - Confirm/Cancel 接口必须用 GORM 的
FirstOrInit+Save实现幂等,而不是Create(否则重试会报唯一键冲突) - 别依赖 Dapr 自动传递 XID:Gin 中间件需手动从
req.Header.Get("X-ID")提取,并存入 context,后续 GORM Hook 才能打标日志 - 本地事务失败时,直接 return 错误;Dapr 会收到 5xx 响应,触发 Cancel 流程——但 Cancel 接口仍要用 GORM 查当前状态再决策,不能无脑反向
最容易被忽略的是:Saga 步骤的超时时间必须短于全局事务超时,且每个 HTTP 调用都要设 context.WithTimeout。GORM 查询没设 timeout,可能卡住整个 Saga 流程,导致 TC 主动回滚而本地已提交。











