分布式事务实战:从 2PC 到 Seata 与本地消息表的完整演进
单体应用里的事务是"本地事务"——一个数据库 ACID 搞定。一旦拆成微服务、数据分散到多个库,跨服务的一致性就成了分布式事务问题。本文梳理分布式事务的经典方案,并给出各自的最佳适用场景。
一、问题的本质:跨资源的一致性
用户下单 → 扣库存(库存服务)→ 扣余额(账户服务)→ 生成订单(订单服务)
任何一个服务失败,都需要让其他服务"撤销"。分布式事务要解决的就是跨服务、跨数据库的原子性。难点在于:没有单一的锁、没有统一的回滚点、网络可能永远不可靠。
二、强一致派:2PC / 3PC
2.1 两阶段提交(2PC)
协调者 → 各参与者:Prepare(准备)
← 各参与者:OK / Abort
协调者 → 各参与者:Commit / Rollback(根据投票结果)
- 阶段一 Prepare:参与者写本地 undo/redo 日志并加锁;
- 阶段二 Commit:全部同意则提交,任一拒绝则回滚。
缺点:同步阻塞(Prepare 后参与者持有锁等待);协调者单点(挂了则参与者无从决策);极端情况数据不一致。
适用:对强一致要求极高、参与方少且可控的内部系统。Java 中对应 Atomikos / XA。
三、最终一致派:TCC 模式
3.1 Try-Confirm-Cancel
把一次业务操作拆成三个阶段:
// 1. Try:预留资源(冻结库存 10 件)
void try(Order order) { inventoryService.freeze(order.item, 10); }
// 2. Confirm:确认执行(真正扣减冻结库存)
void confirm(Order order) { inventoryService.deduct(order.item, 10); }
// 3. Cancel:取消预留(解冻库存)
void cancel(Order order) { inventoryService.unfreeze(order.item, 10); }
优点:无全局锁、性能优于 2PC、业务控制力强。 代价:每个参与方都要实现 Try/Confirm/Cancel 三方法,侵入性高,代码量翻倍。 适用:资金类、库存类等强资源控制的场景。
四、业务层方案:本地消息表
4.1 核心思路
把"发消息"与"业务操作"放在同一个本地事务里,保证"业务成功则消息一定落库":
BEGIN;
-- 1. 业务操作:写订单表
INSERT INTO orders (...) VALUES (...);
-- 2. 业务操作:写本地消息表(同事务!)
INSERT INTO msg_outbox (biz_key, payload, status)
VALUES ('order_123', '{"items":[...]}', 'PENDING');
COMMIT;
// 后台扫描未投递消息,可靠投递到 MQ
for {
rows := db.Query(`SELECT * FROM msg_outbox WHERE status='PENDING' LIMIT 100`)
for _, msg := range rows {
if err := mq.Publish("order.created", msg.Payload); err == nil {
db.Exec(`UPDATE msg_outbox SET status='SENT' WHERE id=?`, msg.ID)
}
}
time.Sleep(1 * time.Second)
}
优点:实现简单、无额外框架、与业务模型天然融合。 缺点:消息表与业务表耦合在同一库中,拆库后需配合 binlog 方案(如 Canal 订阅出库)。
五、最终一致派:消息事务 + 幂等消费
经典流程(RocketMQ 事务消息 / Seata AT 模式):
① 发送半消息(半消息暂不可见)
② 本地事务执行
③ 本地事务成功 → 提交半消息;失败 → 回滚半消息
④ 消费方收到消息,幂等消费
// 消费方幂等消费:以业务唯一键去重
func handleOrderCreated(msg *Message) error {
key := msg.Body.OrderID
if err := db.Exec(`INSERT IGNORE INTO dedup (biz_key) VALUES (?)`, key).Error; err != nil {
return nil // 已处理过,直接忽略
}
return doBusiness(msg) // 真正的业务逻辑
}
关键:消息的"至少一次投递"意味着消费方必须幂等,这是所有最终一致方案的共同前提。
六、Seata:一站式分布式事务框架
Seata 提供多种模式,其中 AT 模式对业务侵入最小:
全局事务 XID 贯穿调用链
业务 SQL 正常执行(无感)
Seata 自动记录前后镜像,冲突时反向补偿
// 全局事务入口(Go 侧常用 dtm 或 seata-go)
gid := seata.BeginGlobalTx(ctx, "order-service")
defer func() {
if err == nil { seata.CommitGlobalTx(gid) } else { seata.RollbackGlobalTx(gid) }
}()
orderService.Create(...) // 参与方自动加入全局事务
paymentService.Pay(...)
适用:已有系统需要分布式事务能力、不想大规模改造业务代码的场景。
七、方案选型决策树
| 业务诉求 | 推荐方案 | |----------|----------| | 强一致、参与方少 | 2PC / XA | | 资金/库存强管控 | TCC | | 追求简单、单库可承载 | 本地消息表 | | 跨多服务、消息驱动 | 事务消息 + 幂等 | | 既有系统少改造 | Seata AT 模式 |
铁律:能不用分布式事务就不用。优先通过业务重设计规避(如把强关联数据放同一服务同一库),分布式事务是最后的武器,不是默认选项。
结语
分布式事务没有银弹:强一致方案牺牲性能与可用性,最终一致方案牺牲实时性,业务方案牺牲开发效率。真正的架构智慧在于识别哪些操作必须强一致,哪些可以最终一致——把必须强一致的操作收敛到最小范围(如资金),把可以异步的操作交给消息与幂等。记住:分布式事务的成本远超你的想象,能在设计阶段拆掉的,绝不在运行时硬扛。
评论(0)
登录 后参与评论。