分布式事务解剖:协议、压测与三大致命陷阱

你有没有半夜被报警惊醒——订单状态是「已支付」,可库存纹丝不动?

这就是分布式事务没兜住底。说实话,处理分布式一致性,就像让你在暴风雨里用纸牌搭房子。每张牌都是独立的数据库,一阵风(网络分区)就能让一切崩塌。可业务却要求所有牌要么同时立着,要么一起倒下。这本来就违反物理定律,对吧?但工程师的浪漫,就是在这种不可能里撕开一道口子。

两阶段提交:一场注定会 Block 的投票

我先讲一个古老的协议——2PC。想象一场婚礼:主持人问新娘「你愿意吗?」新娘羞涩点头。再问新郎,新郎突然掉线了。所有宾客屏住呼吸,主持人也不能宣布婚礼结束,因为新郎还没表态。整个教堂陷入无限等待。这就是2PC的协调者单点之痛和阻塞本质。

在工程实现里,每个资源管理器(RM)都会锁住本地资源,直到协调者(TC)发出全局 commit 或 rollback。如果 TC 挂了,所有参与者就傻等,事务所持的行锁可能数小时不释放。数据库连接池榨干,整个系统瘫痪。MySQL 的 XA 事务就是典型。我曾经在测试环境跑过一个压测:5 个微服务参与同一个 XA 全局事务,每个服务操作自己的数据库。随着并发上升到 200,MySQL 的吞吐量从 3000 TPS 直接腰斩到 1200 TPS,P99 延迟从 10ms 飙升到 2 秒。原因就是锁竞争和网络来回的开销。不夸张,看一眼监控图都能犯心梗。

分布式事务两阶段提交协议协调过程图
分布式事务两阶段提交协议协调过程图

这种同步阻塞的模型在互联网场景几乎被判了死刑。于是悲观锁解决不了的事,就交给乐观补偿。TCC 由此诞生——Try 阶段预留资源,Confirm 真正执行业务,Cancel 释放预留资源。资源不再是数据库行锁,而是业务层面的「冻结」。比如冻结库存、预占优惠券。这像在说:「别真的锁门,只要在门上挂个牌儿说‘有人’,别的请求看到牌子就绕道。」

不过,TCC 把我的头发都坑白了。空回滚、悬挂、幂等,这三个词刻在我每次 code review 的神经上。后面细说。

Saga 与 AT 模式:长事务的救赎?

当业务流程长到令人窒息——比如跨境转账,A 银行扣款后要等 SWIFT 网络确认数小时——TCC 的 Try 阶段很难定义。这时候 Saga 站了出来:将一个长事务拆成多个有序的本地事务,每个本地事务都有对应的补偿操作。像多米诺骨牌,正向流程依次推倒,如果某一步失败,就逆向依次推倒补偿牌。可惜 Saga 不提供隔离性,脏读可能发生,需要在业务层面设计「状态机屏蔽」。

再说 Seata 的 AT 模式——那是我觉得最接近「透明分布式事务」的一次尝试。它自动生成反向 SQL,通过全局锁保证写隔离。想一下:业务代码无侵入,仅需一个 @GlobalTransactional 注解。Seata 的一阶段拦截 SQL,解析出前后镜像(before/after image),生成 undolog;二阶段异步删除 undolog 或回滚补偿。性能对比?我给个内部压测数据:同样是 500 个订单创建,无事务时 4000 TPS,Seata AT 模式加持下 3200 TPS,损失约 20%,但远好于 XA 的腰斩。而且它的锁是全局行锁,粒度细得多。架构之美在于:它巧妙地将数据库的事务能力向上抽象了一层,代价仅仅是多一次 undolog 的持久化。

Seata AT模式一阶段二阶段流程图
Seata AT模式一阶段二阶段流程图

数据不说谎:硬核压测下的生存指南

我从不信 PPT 数据,只信自己跑出来的。一次生产级别压测,目标场景:下单减库存。参与者:订单服务、库存服务,各连独立 MySQL 8.0。事务方案:TCC(基于事务消息) vs Seata AT vs 原生本地事务对比基线。

  • 原生本地事务(单库):8000 TPS,P99=5ms。理想世界的幻觉。
  • TCC(业务补偿):6200 TPS,P99=18ms。需要业务实现 Try/Confirm/Cancel,代码量大,但隔离性通过资源冻结实现,无脏读。
  • Seata AT:5800 TPS,P99=22ms。开发效率高,但二阶段 global commit 异步,一阶段本地事务提交后数据立即可见,存在短暂的脏读窗口,需配合 @GlobalLock 或业务容忍。
  • XA:2700 TPS,P99=1500ms。阻塞式协调,锁持有时间长,连接易耗尽。

数据清清楚楚:别在生产环境用 XA,除非你的并发可以忽略不计。AT 与 TCC 的选择,本质是开发成本与数据一致性强度的 trade-off。AT 的脏读问题在某些金融账务场景不可接受;TCC 又会让业务代码臃肿不堪。这中间的平衡就是架构的艺术。

落入过三次粪坑,才摸到的黄金法则

没有深夜痛哭过的人,不足以谈分布式事务。我踩过的坑比路上水井盖还多,挑三个致命的说:

坑一:空回滚。TCC 中,Try 还没执行,Cancel 却先来了。典型场景:一笔订单 Try 超时,TC 回滚调用 Cancel,但此时 Try 请求可能还在网络拥堵中。解决方案:Cancel 要认可没有 Try 过的情形,返回成功。具体手段——要么用一张事务记录表,Cancel 时发现无记录则直接返回;要么让 Cancel 具备幂等且允许操作未创建的资源。

坑二:悬挂。即 Cancel 比 Try 先到,但之后 Try 又到达,导致资源被意外锁定。比如 Cancel 清理了冻结库存,后到的 Try 又把库存冻结了,而全局事务已经回滚结束,那笔冻结就死在那儿。解决方法:Try 执行前先检查是否有 Cancel 记录,有则拒绝;或者利用插入事务日志配合唯一约束,让 Cancel 插入记录,Try 插入时冲突则告警。

坑三:TC 自身的高可用。很多人过度关注分支事务,却忘了协调器是全局核心。如果 TC 单点,就是 2PC 的复刻。部署必须集群化,Seata Server 也不例外。采用 Raft 组网或用注册中心选举。我们曾因 TC 进程 OOM 重启,导致数分钟内的全局事务悬而不决,最终靠人工脚本清理 undolog 才恢复。从那之后,强制要求 TC 至少三副本,配上监控你的协调器线程池和 RPC 超时。

分布式事务悬挂与空回滚时序图
分布式事务悬挂与空回滚时序图

最佳实践中,我更是形成了一套「反脆弱」打法:所有 TCC 接口强制幂等——以事务 ID 为唯一键;数据库 undolog 表定期归档;AT 模式必须设置合理的全局锁超时;分布式事务链路必须埋点,知道一笔全局事务在各分支的耗时分布。另外,能不用分布式事务就不用。很多业务可通过对账、重试、幂等来最终一致。别一上来就上重武器,锤子看什么都是钉子,最后系统反而脆弱不堪。

回过头看,分布式事务要解决的,根本是「不可靠信使上的确定性共识」。从 2PC 到 Paxos,从 TCC 到 Saga,本质都在寻找一个假设更弱、性能更优的平衡点。没有银弹,只有最适合你业务伤痕的那枚补丁。

免责声明:市场有风险,选择需谨慎!此文仅供参考,不作买卖依据。如有侵权请联系删除。
文章名称:分布式事务解剖:协议、压测与三大致命陷阱
文章链接:https://m.lfdjt.com/info_23_7697.html