【无标题】

位架构师、分布式事务的"填坑人"们,大家好!之前咱们聊了锁,那是单机世界的"独裁者",只要内存和磁盘听你指挥,synchronizedReentrantLock 就能搞定一切。

但到了微服务时代,服务拆分了,数据库也拆分了。订单服务 在 MySQL A,库存服务 在 MySQL B。这时候,你想让 A 和 B 同生共死,就像想让两个分居两地的前男友和前女友同时立刻、马上、绝对地答应你的复合请求一样难。这就是分布式事务

今天,咱们把这层遮羞布扯下来,看看业界为了解决这个问题,到底折腾出了哪些"神器"和"坑"。

第一站:2PC/3PC ------ XA 协议的"霸道总裁"模式

这是最古老、最正统,也最"不招人待见"的方案。XA 协议是分布式事务的鼻祖,它的核心是两阶段提交(2PC)。但大多数人对它的理解仅停留在"准备"和"提交"两个阶段,却忽略了它在数据库内核层面的实现代价。

原理:两阶段提交(2PC)

XA 事务在 MySQL InnoDB 引擎中,不仅仅是逻辑上的两阶段,它涉及到物理日志的写入时机。想象你是一个包工头(协调者),手下有两个小弟(参与者/资源管理器),一个负责砌墙,一个负责刷漆。你要保证要么墙砌好且漆刷好,要么都别干。

  • 阶段一(Prepare)
    • 协调者(TM)向所有参与者(RM)发送 XA PREPARE
    • InnoDB 内核动作
      1. 将事务的修改写入 Redo Log (重做日志),并标记为 XID_PREPARED 状态。
      2. 释放行锁(注意:InnoDB 在 Prepare 阶段会释放行锁,但持有"全局读锁"或特殊的 XA 锁,防止其他事务修改该行直到 XA 提交)。
      3. 返回 XA_OK 给协调者。
  • 阶段二(Commit/Rollback)
    • 协调者收集所有 XA_OK 后,发送 XA COMMIT
    • InnoDB 内核动作
      1. 将 Commit 记录写入 Binlog(如果是主从架构)。
      2. 将 Redo Log 标记为 COMMITTED
      3. 清理 Undo Log。

如果阶段一里有人喊"不能",你就喊:"回滚!" 大家都得把手里的活撤销。

缺点:为什么它是"万恶之源"?
  • 同步阻塞 :在阶段一和阶段二之间,所有小弟都得傻站着 。A 砌完墙了,锁住了砖头,但他不敢走,得等你喊口号。这时候如果有其他人想用砖头?没门!等着吧!这就导致性能极差
  • 单点故障 :如果你这个包工头(协调者)喊完"提交"就挂了,小弟们就懵了。是提交还是回滚?不知道。只能一直锁着资源,导致死锁

除了大家都知道的"同步阻塞"和"单点故障",XA 在 MySQL 中有一个更隐蔽的坑:两阶段提交导致的崩溃恢复问题

  • Crash 场景 :如果 MySQL 在 Redo Log 写入 PREPARE 成功,但 Binlog 还没写入时宕机。
  • 恢复逻辑
    • 重启后,MySQL 检查 Redo Log,发现有 XID_PREPARED
    • 它不知道 Binlog 是否完整写入。
    • 于是 MySQL 会去询问 XA 协调者(如果配置了)或者依赖上层应用层来决定是 Commit 还是 Rollback。
    • 后果 :这就导致了长时间的数据不可用,因为该事务持有的资源(虽然行锁释放了,但逻辑上未完结)会阻塞后续依赖该数据状态的操作。

结论 :XA 是强一致性 的,但它是建立在牺牲高并发性能增加数据库崩溃恢复复杂度 的基础上的。在微服务架构中,除非是极低频的后台操作,否则严禁使用 XA

3PC:加个"预演"阶段

为了缓解阻塞,3PC 加了个"预提交"阶段。

  • 简单说就是:协调者先问"能行吗?",大家说"能";协调者再说"准备提交",大家说"好";最后协调者说"提交"。
  • 如果协调者在"准备提交"阶段挂了,参与者超时后可以根据策略自己决定提交(因为大家都说好准备了)。

但是 ,3PC 引入了更复杂的逻辑,且在网络分区时依然可能导致数据不一致。所以,别太当真,现在很少用原生的 3PC。

第二站:TCC ------ 阿里系的"硬核"补偿模式

TCC(Try-Confirm-Cancel)是互联网大厂(尤其是阿里系)非常喜欢用的模式。它不是靠数据库锁,而是靠业务代码 来保证一致性。把事务控制从数据库层上移到了业务应用层

原理:三步走

还是那个包工头,这次他学精了,不直接干活,先预留 。本质是补偿。它不再是数据库层面的原子操作,而是业务层面的逻辑拆分。

  1. Try(尝试|资源预留)
    • 小弟 A(库存):检查库存够不够?够的话,冻结 100 个(不是扣减,是冻结)。
    • 小弟 B(账户):检查余额够不够?够的话,冻结 500 块。
    • 注意:这里全是预留资源,不产生最终业务影响。
    • 核心动作:检查并锁定资源。
    • 底层实现:在数据库中插入一条"冻结记录"或更新状态为"冻结"。
    • 关键点 :Try 阶段必须保证幂等 ,且必须记录分支事务 ID
  2. Confirm(确认|业务提交)
    • 如果Try阶段大家都成功,协调者喊:"Confirm",核心动作:使用 Try 阶段预留的资源
    • 小弟 A:把冻结的 100 个库存真正扣掉底层实现:将状态从"冻结"更新为"生效"
    • 小弟 B:把冻结的 500 块真正扣掉
    • 约束 :Confirm 操作必须成功。如果失败,框架会无限重试。因此,Confirm 方法内部必须处理幂等(通过事务 ID 去重)。
  3. Cancel(取消|业务回滚)
    • 如果 Try 阶段有人失败(比如余额不足),协调者喊:"Cancel!"
    • 小弟 A:把冻结的库存解冻 (释放)。核心动作:释放 Try 阶段预留的资源。
    • 小弟 B:把冻结的余额解冻底层实现:删除冻结记录或回滚状态。
深度痛点:悬挂与空回滚

这是 TCC 最难处理的地方,也是容易忽略的。

  • 悬挂(Dangling)
    • 场景 :Try 请求因为网络拥堵,比 Cancel 请求晚到
    • 后果:Cancel 先执行,发现没有预留资源,直接返回成功。随后 Try 请求到了,执行了预留。结果事务永远处于"Try 成功"状态,无法回滚,造成资源死锁。
    • 解法 :在 Try 方法执行前,必须查询是否有对应的 Cancel 记录。如果有,说明 Cancel 已经执行过了,拒绝 Try
  • 空回滚(Null Compensation)
    • 场景:Try 请求因为网络原因根本没到,协调者超时直接调用 Cancel。
    • 后果:Cancel 发现没有资源可回滚。
    • 解法:Cancel 方法必须记录"已回滚"的日志,防止后续 Try 请求到达时误操作。
优缺点
  • 优点:性能比 2PC 好,因为 Try 阶段不锁数据库(只是逻辑冻结),Confirm/Cancel 阶段非常快。
  • 缺点代码侵入性极强!你得为每个业务写三个方法(Try, Confirm, Cancel)。如果业务逻辑复杂(比如涉及第三方接口),TCC 会让你写到怀疑人生。

结论 :TCC 性能极高(因为 Try 阶段不锁数据库行,只锁业务逻辑),但代码侵入性极强 。你需要为每个业务写三个方法,还要处理幂等、悬挂、空回滚。适合对一致性要求高、且并发量大的核心链路(如支付、扣库存)。

第三站:本地消息表/事务消息 ------ 最实用的"最终一致性"

这是互联网架构中最主流的方案。它的核心思想是:别想一口吃成个胖子,先保证自己这边不出事。把分布式事务拆解成多个本地事务,通过消息队列进行异步通知。

原理:本地消息表
  • 底层保障 :利用本地数据库的 ACID 特性,保证了业务操作消息发送的原子性。
  1. 订单服务
    • 在业务数据库中创建一张 message
    • 开启本地事务。插入业务数据 + 插入消息记录(状态:待发送)
    • 提交事务。
    • (此时,订单和消息都落地了,原子性保证!)
  2. 异步发送
    • 有一个定时任务(或监听器),扫描 message 表里"待发送"的记录。
    • 把消息发到 MQ。
    • 发送成功后,更新 message 表状态为"已发送"。
  3. 下游消费
    • 库存服务监听 MQ,收到消息后扣减库存。
升级版:RocketMQ 事务消息

RocketMQ 把这个过程封装得更优雅。

  • 生产者先发一个半消息(对消费者不可见)。
  • MQ 返回成功。
  • 生产者收到 MQ 的 ACK 后,执行本地事务(订单入库)
  • 如果本地事务成功,告诉 MQ 提交,消息对消费者可见。
  • 如果失败,告诉 MQ 回滚,消息删除。
  • 兜底 :如果生产者挂了,MQ 没收到确认,会反过来查生产者:"兄弟,刚才那条消息咋样了?"(回查机制)定期扫描那些长时间处于"半消息"状态的消息
    • 回调生产者的 checkLocalTransaction 接口,询问:"兄弟,这条消息对应的本地事务到底成功没?"。
    • 生产者查数据库,返回状态

评价 :这是最实用 的方案,解耦彻底,性能极高,只要你能接受"最终一致性"。适合非实时性要求高、但数据量大的场景(如积分、通知、大数据同步)

第四站:Seata ------ AT 模式的"无侵入"魔法

Seata 是阿里开源的分布式事务框架,它的 AT 模式 是目前 Java 界最火的方案,因为它几乎不用改代码

原理:自动代理 + 逆向日志

Seata AT 模式其实是对 2PC 的一种优化,它把"脏活累活"都交给了框架。

  1. 阶段一

    • 拦截 SQL :Seata 拦截业务 SQL(如 UPDATE account SET money = money - 100 WHERE id = 1)。
    • 查询前镜像(Before Image) :在执行更新前,Seata 自动执行 SELECT * FROM account WHERE id = 1,拿到旧数据(money=200)。
    • 执行 SQL:执行真正的更新操作(money=100)。
    • 查询后镜像(After Image):执行后查询新数据(money=100)。
    • 生成 Undo Log :将前镜像、后镜像、SQL 信息组装成一条 Undo Log 记录,插入到 undo_log 表。
    • 本地提交:将业务数据和 Undo Log 在同一个本地事务中提交。
    • 释放本地锁:注意,此时本地数据库锁已经释放!
    • 汇报:向 Seata Server(TC)汇报"我准备好了"。
  2. 阶段二(Commit/Rollback)

    • Commit :异步删除 undo_log。速度极快。
    • Rollback
      1. Seata Server 发送回滚指令。
      2. 参与者根据 undo_log 中的前镜像 ,生成反向 SQL(UPDATE account SET money = 200 WHERE id = 1)。
      3. 执行反向 SQL,恢复数据。
深度痛点:脏写(Dirty Write)

Seata AT 最大的问题是隔离性差

  • 场景

    • 事务 A 修改了数据(money -100),提交了本地事务(释放了行锁),但全局事务还没提交。
    • 此时,事务 B 读取了这笔数据(money=100),并进行了修改(money -50),提交了。
    • 如果事务 A 此时回滚,它会恢复数据到 200。
    • 结果 :事务 B 的修改(-50)被事务 A 的回滚覆盖了!这就是脏写
  • Seata 的解法:全局锁(Global Lock)

    • 在阶段一提交本地事务时,Seata 会尝试获取全局锁 (在 global_table 中加锁)。
    • 如果拿不到全局锁(说明有其他全局事务在操作同一行),本地事务会重试,直到拿到锁才提交。
    • 代价 :虽然避免了脏写,但性能下降,退化成类似 XA 的串行化。
优缺点
  • 优点无侵入 !业务代码只需要加一个 @GlobalTransactional 注解,完全不用写 Try/Confirm 代码。
  • 缺点 :因为依赖数据库的本地事务和行锁,并发度受限。如果两个事务同时操作同一行数据,还是会锁住。

总结:怎么选?

  • 强一致性需求(极少) :选 Seata ATXA。但要做好性能下降的心理准备。
  • 高并发、核心业务(推荐) :选 TCC。虽然代码写得累,但性能可控,逻辑清晰。
  • 高并发、非核心/异步业务(最常用) :选 本地消息表/MQ 事务消息。这是互联网架构的基石,用"时间换空间",实现最终一致性。
方案 一致性 性能 侵入性 隔离性 适用场景
XA (2PC) 强一致 低(阻塞) 传统单体拆微服务初期,低频后台任务
TCC 最终一致 高(需写三个方法) 高(业务控制) 核心交易链路,高并发,对一致性要求高
MQ 事务 最终一致 极高(异步) 中(需发消息) 跨系统解耦,非实时业务(积分、通知)
Seata AT 最终一致 中(依赖全局锁) 低(注解) 中(有脏写风险) 内部微服务,开发效率优先,并发适中

最后送你一句话

"分布式事务没有银弹。所有的方案都是在一致性可用性性能之间做交易。作为架构师,你的任务不是寻找完美的方案,而是找到最适合你业务场景的那个'平衡点'。"

相关推荐
Yvonne爱编码2 小时前
数据库---Day6 数据库约束
数据库
小江的记录本2 小时前
【Swagger】Swagger系统性知识体系全方位结构化总结
java·前端·后端·python·mysql·spring·docker
她的男孩2 小时前
ForgeAdmin实战:开源项目分布式幂等组件 v2.0 升级
后端
空太Jun2 小时前
Spring Security 自定义数据库认证(初尝试)
java·数据库·spring
她的男孩2 小时前
ForgeAdmin渐进式 Spec 开发:开源项目从需求到落地完整流程
后端
原燊炜2 小时前
Struts2_拦截器_登录拦截
后端
sinat_255487812 小时前
泛型·学习笔记
java·jvm·数据库·windows·python
wregjru2 小时前
【MySQL】4. 数据约束详解
数据库·sql·oracle