前言

在微服务架构、分库分表的今天,业务逻辑经常需要跨多个MySQL实例操作数据。比如电商下单:扣库存(库存库)、生成订单(订单库)、扣余额(账户库)——这三个库的操作必须“要么全成功,要么全失败”,否则就会出现“库存扣了但订单没生成”的尴尬局面。这就是典型的分布式事务问题。

今天咱们就来聊聊MySQL分布式事务的核心实现方案(XA事务),以及它的适用场景、避坑指南,还有替代方案。看完这篇,你不仅能搞懂原理,还能知道怎么在实际项目中落地!

一、分布式事务的核心矛盾:跨库如何保证ACID?

1. 什么是分布式事务?

分布式事务是跨多个独立数据库(或服务)的事务,需要保证这些数据库操作的原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。简单说,就是“跨库操作,不能部分成功”。

2. 跨库事务的三大痛点

  • 网络不可靠:跨节点通信可能超时、丢包,导致“部分提交”(比如A库提交了,B库挂了)。
  • 数据不一致:各库事务提交时间不同,可能被外部看到“半成品”状态(比如用户看到库存扣了但订单没生成)。
  • 故障恢复难:某个节点宕机后,如何回滚或补全未完成的事务?

二、MySQL分布式事务的“官方方案”:XA事务

MySQL从5.0版本开始支持XA事务(遵循X/Open XA规范),通过两阶段提交(2PC)协议协调多个数据库(资源管理器RM),是目前最“正统”的跨库事务解决方案。

1. XA事务的两大角色

  • 事务管理器(TM):全局指挥官,负责决定事务是提交还是回滚,并通知所有参与数据库(RM)。
  • 资源管理器(RM):每个MySQL实例都是一个RM,负责执行本地事务,并向TM汇报状态(“我准备好了”或“我要凉”)。

2. 两阶段提交(2PC)的详细流程

2PC是XA事务的核心,分为“准备阶段”和“提交/回滚阶段”,咱们用“下单扣库存+生成订单”的例子来拆解:

阶段1:准备阶段(Prepare)

TM先问所有RM:“你们能搞定自己的操作吗?”

  • 步骤1:TM给库存库(RM1)和订单库(RM2)发XA START 'xid1',开启全局事务(xid1是全局唯一事务ID)。
  • 步骤2:库存库执行UPDATE stock SET count=count-1 WHERE id=1,订单库执行INSERT INTO order (user_id, amount) VALUES (1, 100)
  • 步骤3:库存库和订单库分别执行XA END 'xid1'XA PREPARE 'xid1',表示“本地操作已完成,数据已持久化,等待最终指令”。
  • 步骤4:RM1和RM2向TM反馈:“我准备好了!”(READY)。
阶段2:提交/回滚阶段(Commit/Rollback)

TM根据所有RM的反馈做决定:

  • 如果所有RM都READY:TM发XA COMMIT 'xid1',库存库和订单库正式提交事务,数据持久化。
  • 如果有任意RM失败(比如订单库宕机):TM发XA ROLLBACK 'xid1',库存库和订单库回滚本地操作,回到初始状态。

3. MySQL XA事务的SQL实操

以InnoDB引擎为例(需确保innodb_support_xa=ON,默认开启),咱们直接看代码:

-- TM开启全局事务(xid1是自定义的全局事务ID)
XA START 'xid1';

-- RM1(库存库)执行本地操作
UPDATE stock SET count = count - 1 WHERE product_id = 1;

-- RM2(订单库)执行本地操作
INSERT INTO `order` (user_id, total) VALUES (1, 99.9);

-- 所有本地操作完成后,标记为“准备提交”
XA END 'xid1';       -- 结束本地事务关联
XA PREPARE 'xid1';   -- 通知TM:我准备好了!

-- (假设所有RM都返回READY)
-- TM决定提交全局事务
XA COMMIT 'xid1';    -- 所有RM正式提交

-- (如果某个RM失败,比如订单库挂了)
XA ROLLBACK 'xid1';  -- 所有RM回滚,库存回滚+订单不生成

4. 悬挂事务的恢复:TM宕机怎么办?

如果TM在阶段2提交前宕机,重启后需要检查是否有未完成的XA事务。这时候可以通过XA RECOVER命令查看“悬挂”的事务,并手动提交或回滚:

-- 查看未完成的XA事务(返回格式:格式ID, 全局事务ID, 分支事务ID, 格式ID)
XA RECOVER;

-- 假设xid1未完成,手动提交
XA COMMIT 'xid1';

-- 或者回滚
XA ROLLBACK 'xid1';

三、XA事务的“两面性”:适用场景与局限性

1. 适合XA的场景

  • 跨MySQL实例的事务:比如分库分表后,主库和从库的操作需要原子性。
  • 强一致性要求:金融转账、订单支付等场景,必须“要么全对,要么全错”。
  • 短事务:2PC的性能开销大(多次网络交互+锁持有),适合执行时间短的事务(比如几秒内完成)。

2. XA的“硬伤”

  • 性能差:2PC需要多次网络通信(TM↔RM),且事务提交前要锁定资源,高并发下容易成瓶颈。
  • 锁持有时间长:准备阶段RM会锁定数据,直到事务提交/回滚,可能导致其他请求阻塞。
  • 单点风险:TM是全局协调者,如果TM宕机且没持久化状态,可能导致事务无法恢复(不过MySQL的XA事务日志能缓解这个问题)。

四、XA之外的选择:TCC、Saga、消息队列

如果XA的性能或灵活性不够,咱们还有其他方案,各有各的适用场景:

1. TCC(Try-Confirm-Cancel):业务层的补偿

核心思路:把事务拆成3步:

  • Try:预留资源(比如扣库存时,先冻结1个库存,而不是直接扣减)。
  • Confirm:确认提交(所有Try成功后,正式扣减冻结的库存)。
  • Cancel:回滚释放(某个Try失败,解冻之前预留的资源)。

适用场景:长事务、业务可补偿(比如电商大促时的库存扣减)。
与MySQL结合:每个Try/Confirm/Cancel操作都通过MySQL本地事务完成,比如:

-- Try:冻结库存
UPDATE stock SET count = count - 1, frozen = frozen + 1 WHERE product_id = 1;

-- Confirm:正式扣减(解冻时不操作,直接扣)
UPDATE stock SET count = count - 1, frozen = frozen - 1 WHERE product_id = 1;

-- Cancel:解冻库存
UPDATE stock SET frozen = frozen - 1 WHERE product_id = 1;

2. Saga模式:长事务的“后悔药”

核心思路:把长事务拆成多个本地子事务,每个子事务对应一个补偿操作。如果某一步失败,反向执行已成功的子事务的补偿。

适用场景:业务流程长、允许最终一致(比如用户下单→支付→物流追踪)。
与MySQL结合:每个子事务用MySQL本地事务实现,补偿操作同样用MySQL事务。例如:

步骤 操作 补偿操作
子事务1 扣库存(MySQL事务) 恢复库存(MySQL事务)
子事务2 生成订单(MySQL事务) 取消订单(MySQL事务)
子事务3 扣账户余额(MySQL事务) 退回余额(MySQL事务)

3. 可靠消息队列(RocketMQ事务消息):异步解耦

核心思路:通过消息队列保证“本地事务执行”和“消息发送”的原子性,下游服务消费消息并执行操作。

适用场景:异步解耦、最终一致(比如用户注册后发送短信)。
与MySQL结合:本地事务执行后,发送“预消息”到MQ;MySQL记录消息状态(已发送/未发送);MQ确认消息后,下游服务消费并执行操作。

4. Seata框架:一站式解决方案

Seata是阿里开源的分布式事务框架,支持AT(自动补偿)、TCC、Saga等模式,兼容MySQL。它通过“事务协调器(TC)”替代手动TM,简化了开发。

适用场景:微服务架构下的跨服务事务(比如订单服务→库存服务→账户服务)。

五、总结:怎么选?

  • 强一致性+短事务:选XA事务(MySQL原生支持,简单直接)。
  • 高并发+长事务:选TCC或Saga(业务层补偿,减少锁竞争)。
  • 异步解耦:选可靠消息队列(比如RocketMQ事务消息)。
  • 微服务架构:选Seata(框架封装,降低开发成本)。

最后提醒:无论选哪种方案,都要做好日志记录故障恢复!比如XA事务的INNODB_XA_TRANSACTIONS表要定期检查,TCC的补偿操作要幂等,消息队列要保证“至少一次”投递。分布式事务没有银弹,只有最适合业务的方案!

Logo

更多推荐