1. 为什么本地事务不够
下单要扣库存、加积分、创订单,三个服务各管各的库。一个 @Transactional 管不了三个库。跨服务要么两阶段提交(CP 但锁资源、可用性差),要么最终一致(AP,推荐)。
2. Saga:正向步骤 + 补偿
把大事务拆成一串本地事务,每步有对应的补偿(回滚)操作:
steps := []SagaStep{ {Do: deductStock, Compensate: restoreStock}, {Do: createOrder, Compensate: cancelOrder}, {Do: addPoints, Compensate: removePoints}, } // 任一步失败,反向执行前面已完成的补偿
扣库存成功、建订单失败,就调 restoreStock 把库存加回去。
3. 用事件编排 Saga
更松耦的写法:每步完成发事件,下一步订阅后执行,失败时发补偿事件:
js.Publish("stock.deducted", evt) // 订单服务订阅,建订单;失败则发 order.failed → 库存订阅补偿
编排(集中)还是协同(事件),看复杂度。步骤多、要集中可视选编排;想解耦选协同。
4. 事务消息
保证「本地落库」和「发事件」同时成功,不丢事件:
// 本地消息表:同一事务写业务表和消息表 tx.Exec("INSERT order ...") tx.Exec("INSERT outbox (topic,payload) ...") tx.Commit() // 独立 worker 扫 outbox 发消息,发成功删记录
发消息失败重试,保证下游最终收到。Kafka 的事务消息也能做类似语义。
5. 读已写一致
最终一致下,下单后立刻查可能查不到积分。前端要么接受延迟,要么用「下单成功后轮询 / WebSocket 推送完成」(第 11 篇)代替立即查。
6. 上线清单
能本地事务解决的别上分布式。优先最终一致(Saga/事务消息),少碰 2PC。补偿操作必须幂等,可能被执行多次。Saga 步骤设计成可补偿、可重试。用 outbox 模式保证事件不丢。监控 Saga 卡住的实例,人工介入。
下一篇讲注册发现与配置中心,服务多了怎么管。