在微服务架构成为主流的今天,一个完整的业务操作往往需要跨多个数据库实例甚至不同服务。我经历过一个电商订单系统的重构项目:创建订单需要同时操作订单库、库存库和用户积分库,这三个数据库分别部署在不同服务器上。当库存扣减成功但积分扣除失败时,就会产生数据不一致的问题。
分布式事务要解决的核心问题就是保证这种跨资源操作的ACID特性,特别是原子性(Atomicity)和一致性(Consistency)。MySQL作为最广泛使用的关系型数据库,提供了几种典型的解决方案:
关键考量点:强一致性要求越高,系统可用性和性能往往越低。根据CAP理论,网络分区(P)发生时,必须在一致性(C)和可用性(A)之间做出选择。
XA协议的核心是两阶段提交(2PC),我通过一个转账场景来说明其工作原理:
准备阶段(Prepare):
提交阶段(Commit/Rollback):
sql复制-- MySQL中XA事务的典型SQL序列
XA START 'transaction_id'; -- 开启XA事务
UPDATE account SET balance = balance - 100 WHERE user_id = 1;
XA END 'transaction_id';
XA PREPARE 'transaction_id'; -- 第一阶段准备
XA COMMIT 'transaction_id'; -- 第二阶段提交
在实际项目中,我们发现XA协议存在几个典型问题:
同步阻塞问题:
单点故障风险:
数据不一致场景:
实战经验:XA事务不适合高并发场景。我们在支付系统中测试发现,当TPS超过500时,XA事务的失败率会显著上升。对于核心支付链路,我们最终采用了XA+异步补偿的混合方案。
TCC(Try-Confirm-Cancel)是我们目前在订单系统中使用的主要方案。以创建订单为例:
Try阶段:
Confirm阶段:
Cancel阶段:
java复制// TCC模式的典型代码结构
public class OrderService {
@Transactional
public void createOrder(OrderDTO orderDTO) {
// Try阶段
inventoryService.freezeStock(orderDTO.getItems());
pointsService.freezePoints(orderDTO.getUserId(), orderDTO.getPoints());
orderMapper.createTemporaryOrder(orderDTO);
// 记录TCC上下文
TccContext context = new TccContext();
context.setXid(TransactionContext.getXID());
tccLogService.save(context);
}
@Transactional
public void confirmOrder(String xid) {
TccContext context = tccLogService.get(xid);
inventoryService.confirmStock(context.getItems());
pointsService.confirmPoints(context.getUserId());
orderMapper.confirmOrder(context.getOrderId());
}
@Transactional
public void cancelOrder(String xid) {
TccContext context = tccLogService.get(xid);
inventoryService.cancelStock(context.getItems());
pointsService.cancelPoints(context.getUserId());
orderMapper.cancelOrder(context.getOrderId());
}
}
对于长流程业务(如机票+酒店套餐预订),我们采用SAGA模式:
mermaid复制graph LR
A[开始] --> B[机票预订]
B --> C{成功?}
C -->|是| D[酒店预订]
C -->|否| E[取消机票]
D --> F{成功?}
F -->|是| G[完成]
F -->|否| H[取消酒店]
H --> E
避坑指南:SAGA模式必须保证每个补偿操作是幂等的。我们曾遇到因网络重试导致的多次补偿,后来通过为每个操作添加唯一执行令牌(token)解决了这个问题。
这是我们最常用的最终一致性方案,核心流程:
sql复制-- 本地消息表设计示例
CREATE TABLE transaction_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_id VARCHAR(64) NOT NULL COMMENT '业务ID',
topic VARCHAR(128) NOT NULL COMMENT '消息主题',
content TEXT NOT NULL COMMENT '消息内容',
status TINYINT NOT NULL COMMENT '0-待发送 1-已发送 2-已消费',
retry_count INT DEFAULT 0 COMMENT '重试次数',
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
UNIQUE KEY uk_biz_topic (biz_id, topic)
) ENGINE=InnoDB;
对于高吞吐场景,我们采用RocketMQ的事务消息:
java复制// RocketMQ事务消息示例
public class OrderService {
private TransactionMQProducer producer;
@Transactional
public void createOrder(OrderDTO orderDTO) {
// 1. 创建订单
orderMapper.insert(orderDTO);
// 2. 发送事务消息
Message msg = new Message("order_topic", JSON.toJSONBytes(orderDTO));
TransactionSendResult result = producer.sendMessageInTransaction(msg, orderDTO);
// 3. 处理发送结果
if (!result.getLocalTransactionState().equals(LocalTransactionState.COMMIT_MESSAGE)) {
throw new RuntimeException("消息发送失败");
}
}
}
// 事务监听器
public class OrderTransactionListener implements TransactionListener {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
OrderDTO orderDTO = (OrderDTO) arg;
// 执行本地事务(如扣减库存)
inventoryService.deductStock(orderDTO.getItems());
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 检查本地事务状态
String orderId = msg.getKeys();
Order order = orderMapper.selectById(orderId);
return order != null ? LocalTransactionState.COMMIT_MESSAGE
: LocalTransactionState.ROLLBACK_MESSAGE;
}
}
消息重复消费:
消息积压处理:
事务状态不一致:
根据我们的实践经验,不同业务场景适合不同方案:
| 业务类型 | 一致性要求 | 吞吐量要求 | 推荐方案 | 典型案例 |
|---|---|---|---|---|
| 金融核心交易 | 强一致 | 中等 | XA+异步核对 | 支付、转账 |
| 电商订单 | 最终一致 | 高 | TCC+消息队列 | 下单、库存扣减 |
| 物流跟踪 | 最终一致 | 极高 | 本地消息表+SAGA | 状态更新、轨迹记录 |
| 用户行为统计 | 弱一致 | 极高 | 最大努力通知 | 点击、浏览记录 |
我们的交易系统采用了分层事务策略:
支付核心层:
订单履约层:
积分和通知层:
XA协议优化:
TCC模式优化:
消息方案优化:
个人经验:没有任何一种方案能解决所有问题。我们现在的策略是:能用本地事务就不用分布式事务;必须用分布式事务时,优先考虑最终一致性方案;只有对资金、库存等核心数据才使用强一致性方案。