GORM本身不支持跨进程事务,必须通过Saga模式+状态表兜底:将各服务的GORM本地事务作为Saga步骤,用共享saga_state表记录进度与补偿指针,嵌入式编排复用DB连接池,补偿需为带校验的新业务事务而非简单逆向SQL。

事务跨进程时GORM本身无解,必须靠Saga状态表兜底
GORM的 Transaction 函数只在单个数据库连接内有效,跨进程意味着跨数据库实例、跨服务边界,GORM连连接都拿不到,更别说传播事务上下文。所谓“统筹”,不是靠ORM扩展,而是把GORM本地事务作为Saga每个步骤的执行单元,用一张共享的 saga_state 表记录全局进度和补偿指针。
常见错误现象:有人试图用 db.Session(&gorm.Session{Context: ctx}) 把context透传到其他服务,结果发现事务状态完全不生效——因为context里没有携带任何实际的数据库会话句柄,只是空壳。
- 必须将每个参与服务的GORM操作封装为独立、可重入的本地事务块,不依赖上游事务状态
-
saga_state表至少包含字段:id(Saga ID)、step(当前执行到第几步)、status(running/compensating/finished)、created_at、updated_at - 所有正向操作成功后,必须先持久化该步骤状态再调用下一步,否则进程崩溃即丢失进度
为什么Saga编排不能放在独立协调器里?两跳网络+单点故障是硬伤
独立协调器看似职责清晰,但在下单这类高并发链路中,它直接把P99延迟从60ms拉到220ms。根本原因不是代码写得差,而是每次调用都要经历:订单服务 → 协调器(序列化+网络)→ 库存服务(反向RPC),中间任意一跳超时或GC停顿,整个Saga就卡死。
去年一次线上事故就是协调器节点GC停顿3秒,导致支付回调堆积、库存预占状态滞留,最终触发大量人工对账。这不是配置问题,是架构层面的耦合放大。
- 嵌入式编排(协调逻辑写在订单服务内)能复用现有DB连接池和事务上下文,避免额外RPC
- 前提条件是业务库可作为事实源——即
saga_state和订单主表同库,靠本地事务保证状态写入与业务操作原子性 - 若库存/支付等服务无法提供幂等回调接口,嵌入式编排反而更易控制重试节奏和补偿触发时机
RunSaga同步函数只能用于单机调试,生产必须拆成状态驱动循环
知识库里那个简洁的 RunSaga 函数看着漂亮,但它在进程崩溃后完全不可恢复,也没有补偿失败重试机制。生产环境必须改成基于状态轮询的驱动模型:启动一个后台goroutine,定期查 saga_state 表中 status = 'running' 的记录,按 step 字段决定执行下一步还是触发补偿。
容易踩的坑是“查完就读取、读完就更新”,没加行锁或乐观锁,导致两个worker同时处理同一条Saga记录,引发重复扣库存或双退款。
- 查询时必须加
SELECT ... FOR UPDATE或用WHERE status = 'running' AND version = ?做乐观更新 - 每步执行前检查是否已成功(通过查询业务表状态,而非仅依赖
saga_state.step) - 补偿失败不能静默跳过,要进死信队列并告警,比如退款接口连续5次返回“余额不足”,需人工介入
补偿操作不是“逆向SQL”,而是带业务语义的新事务
很多人写补偿直接 UPDATE inventory SET stock = stock + 10 WHERE sku_id = ?,这是危险的。真正的补偿必须是完整业务动作:比如“归还预占库存”,要校验该笔预占是否仍有效、是否已被实占、是否有并发释放请求,再生成一条 inventory_release 记录,最后才更新可用库存。
否则会出现空补偿(正向没执行,补偿先跑了)或悬挂(正向执行了但状态未写入,补偿又没触发)。
- 每个补偿函数必须接受原始请求参数(如
order_id,sku_id,quantity),不能只靠Saga ID反查 - 补偿入口要校验幂等键,例如
compensation_key = "release_" + order_id + "_" + sku_id - GORM里补偿事务要用新
*gorm.DB实例,避免复用正向事务的连接和hook,防止状态污染
关键点其实就一个:Saga不是让GORM变分布式的工具,而是用GORM把每个服务的本地事务“钉”在业务语义上,再靠状态表和重试机制把它们串起来。最常被忽略的是补偿操作的业务完整性——它不是回滚,是一次新的、带校验的、可审计的业务动作。











