单体数据库的事务通过 ACID 保证,但当数据分布在多个数据库或服务中时,如何保证跨节点的事务一致性?分布式事务是微服务架构和分库分表场景下的核心挑战。本文全面解析主流分布式事务协议和工程实践。
1. 分布式事务的核心问题
1.1 CAP 定理的约束
CAP 定理:在分布式系统中,Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性)三者不可兼得。
分布式事务的设计本质是在 C 和 A 之间做权衡。
1.2 典型场景
-- 下单场景(跨服务)
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Order │ │ Inventory │ │ Account │
│ Service │ │ Service │ │ Service │
│ DB: order │ │ DB: stock │ │ DB: acct │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
└─────────────────┴─────────────────┘
需要原子性:全部成功 或 全部失败
2. 2PC 两阶段提交
2.1 协议流程
Phase 1 (Prepare):
Coordinator Participants (A/B/C)
│ │
│─── 1a. Prepare Request ──►││
│ │
│◄── 1b. Yes/No Response ────││
│ │
│ 所有回答 Yes → Phase 2 Commit
│ 任一回答 No → Phase 2 Rollback
Phase 2 (Commit/Rollback):
│─── 2a. Commit/Rollback ──►││
│ │
│◄── 2b. ACK ────────────────││
2.2 XA 规范
XA 是 X/Open 组织提出的分布式事务标准接口:
// XA 接口
public interface XAResource {
void start(Xid xid, int flags); // 开始事务分支
void end(Xid xid, int flags); // 结束事务分支
int prepare(Xid xid); // 第一阶段:准备
void commit(Xid xid, boolean onePhase); // 第二阶段:提交
void rollback(Xid xid); // 第二阶段:回滚
}
MySQL XA 使用:
-- 会话 1(参与者A)
XA START 'order-xid';
INSERT INTO orders VALUES (...);
XA END 'order-xid';
XA PREPARE 'order-xid'; -- 第一阶段:准备
-- 协调器收到所有 PREPARE OK
XA COMMIT 'order-xid'; -- 第二阶段:提交
-- 或 XA ROLLBACK 'order-xid'
2.3 2PC 的问题
| 问题 | 说明 | 影响 |
|---|---|---|
| 同步阻塞 | 参与者需锁定资源等待协调器指令 | 性能差 |
| 单点故障 | 协调器宕机,参与者一直悬停 | 数据不一致风险 |
| 数据不一致 | 协调器发出 Commit 后宕机,部分参与者没收到 | 部分提交 |
| 脑裂 | 网络分区导致部分提交、部分回滚 | 严重数据错误 |
3. 3PC 三阶段提交
3.1 改进点:引入超时与 CanCommit
Phase 1 (CanCommit):
│──► CanCommit? ──► 检查资源是否可用,不锁定
│◄── Yes/No ◄────── 参与者预检查
Phase 2 (PreCommit):
│──► PreCommit ──► 预锁定资源
│◄── ACK ◄──────── 参与者确认
Phase 3 (DoCommit):
│──► DoCommit ──► 真正提交
│◄── ACK ◄─────── 参与者确认
3.2 3PC vs 2PC
| 维度 | 2PC | 3PC |
|---|---|---|
| 阶段数 | 2 | 3 |
| 阻塞 | 完全阻塞 | 引入超时,减少阻塞时间 |
| 单点故障 | 严重 | 协调器宕机可通过超时继续 |
| 复杂度 | 简单 | 复杂 |
| 使用广泛度 | 广泛应用(XA) | 较少使用 |
3PC 实际工程中很少使用,超时机制在复杂网络下仍然不可靠。
4. TCC(Try-Confirm-Cancel)
4.1 核心思想
TCC 将每个操作拆分为三个阶段:
| 阶段 | 操作 | 语义 |
|---|---|---|
| Try | 预留资源 | 执行业务检查,锁定/冻结资源 |
| Confirm | 确认执行 | 真正执行业务(幂等) |
| Cancel | 取消回滚 | 释放预留资源(幂等) |
4.2 电商下单示例
// 库存服务
public interface InventoryService {
@TccMethod
boolean tryDeduct(String productId, int count); // 冻结库存
@TccMethod
boolean confirmDeduct(String productId, int count); // 真正扣减
@TccMethod
boolean cancelDeduct(String productId, int count); // 释放冻结
}
// 订单服务编排
@Service
public class OrderTccService {
@Autowired InventoryService inventory;
@Autowired AccountService account;
public void placeOrder(Order order) {
// Phase 1: Try - 全部预留资源
boolean stockOk = inventory.tryDeduct(order.getProductId(), order.getCount());
boolean payOk = account.tryFreeze(order.getUserId(), order.getAmount());
if (stockOk && payOk) {
// Phase 2: Confirm - 全部确认
inventory.confirmDeduct(order.getProductId(), order.getCount());
account.confirmPay(order.getUserId(), order.getAmount());
} else {
// Phase 2: Cancel - 全部回滚
if (stockOk) inventory.cancelDeduct(order.getProductId(), order.getCount());
if (payOk) account.cancelPay(order.getUserId(), order.getAmount());
}
}
}
4.3 TCC 的优缺点
| 优点 | 缺点 |
|---|---|
| 无全局锁,并发性能好 | 业务侵入性强,需实现三阶段的业务逻辑 |
| Try 阶段可回滚,灵活性高 | 幂等性需业务保证 |
| 适合短事务 | Confirm/Cancel 需保证最终执行 |
| 相比 2PC,资源锁定时间短 | 开发成本高 |
4.4 空回滚与幂等
// 空回滚:Try 超时未执行,Cancel 先到达
public boolean cancelDeduct(String productId, int count) {
String record = getTransactionRecord();
if (record == null) {
// Try 未执行或已回滚 → 空回滚,直接返回成功
return true;
}
// 正常回滚逻辑
}
// 幂等:Confirm/Cancel 可能被重试多次
public boolean confirmDeduct(String productId, int count) {
if (alreadyConfirmed()) return true; // 幂等检查
// 确认逻辑
}
5. Saga 长事务
5.1 核心思想
Saga 将长事务拆分为本地事务序列,每个本地事务提交后立即释放资源。如果任一失败,执行补偿事务回滚已完成的步骤。
正向流程: T1 → T2 → T3 → T4 → 完成
│
失败回滚: T1 → T2 → T3 ✗ → C3 → C2 → C1
5.2 两种实现模式
| 模式 | 描述 | 适用 |
|---|---|---|
| 编排式(Choreography) | 每个服务完成本地事务后发送事件,触发下一个服务 | 简单流程 |
| 协调式(Orchestration) | 中央 Saga 协调器按顺序调用各服务 | 复杂流程 |
5.3 Saga 案例:旅行预订
正向事务:
T1: 预订航班 → COMMIT
T2: 预订酒店 → COMMIT
T3: 预订租车 → COMMIT
T4: 处理支付 → COMMIT
T3 失败时的补偿:
C3: 取消租车预订 → 忽略(未执行)
C2: 取消酒店预订 → COMMIT
C1: 取消航班预订 → COMMIT
5.4 Saga 的隔离性缺失
Saga 不提供隔离性,存在更新丢失和脏读风险:
T1: 扣减库存 10 → COMMIT
T2: 读取库存(看到已扣减后的值)→ 基于错误值做决策
T1 回滚 → 补偿增加库存 10
结果:T2 的决策基于了一个从未真正提交的数据 → 业务异常
缓解方案:
- 语义锁:业务层面标记"处理中"状态
- 悲观视图:先查后改时考虑补偿事务的可能性
- 可交换更新:更新操作具有交换律(如金额 ± 可重排)
6. 消息最终一致性
6.1 本地消息表
-- 本地事务内同时写入业务表 + 消息表
BEGIN;
INSERT INTO orders (...) VALUES (...);
INSERT INTO message_queue (topic, payload, status)
VALUES ('inventory', '{"productId":1,"count":2}', 'PENDING');
COMMIT;
-- 定时任务扫描 PENDING 消息,投递到 MQ
-- 消费者处理成功后,消息标记为 DONE
6.2 事务消息(RocketMQ)
// RocketMQ 事务消息
TransactionMQProducer producer = new TransactionMQProducer("order_group");
// 半消息:对消费者不可见
Message msg = new Message("order_topic", "减库存", bytes);
producer.sendMessageInTransaction(msg, new LocalTransactionExecuter() {
@Override
public LocalTransactionState executeLocalTransactionBranch(Message msg, Object arg) {
try {
orderService.createOrder((Order) arg); // 本地事务
return LocalTransactionState.COMMIT_MESSAGE; // 确认消息
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE; // 回滚消息
}
}
});
事务消息流程:
- 发送半消息(对消费者不可见)
- 执行本地事务
- 本地事务成功 → 确认消息(消费者可见)
- 本地事务失败 → 回滚消息(消费者不可见)
- 如果本地事务执行状态未知 → MQ 回调查询
6.3 最大努力通知
服务A ──► 消息队列 ◄── 服务B(消费处理)
│ │
│◄──────────┘(失败重试,有限次数)
│
└── 定时对账系统(A/B 数据一致性校验)
适用于:对实时性要求不高、可事后对账补偿的场景(如支付回调)。
7. Seata 框架实战
7.1 Seata 架构
┌─────────────────────────────────────────────────────────┐
│ Seata Server │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ TC │ │ TM │ │ RM │ │
│ │ Transaction │ │ Transaction │ │ Resource │ │
│ │ Coordinator │ │ Manager │ │ Manager │ │
│ │ 事务协调器 │ │ 事务管理器 │ │ 资源管理器 │ │
│ └──────────────┘ └──────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
TC: 独立部署,维护全局事务状态
TM: 在应用端(@GlobalTransactional 注解处)
RM: 在各参与服务的资源层(代理数据源)
7.2 AT 模式(Automatic Transaction)
Seata AT 模式对业务零侵入,自动代理数据源:
@Service
public class OrderService {
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder(Order order) {
orderService.create(order); // RM: order_db
inventoryService.deduct(order); // RM: inventory_db
accountService.debit(order); // RM: account_db
}
}
AT 模式原理:
- 一阶段:执行业务 SQL,记录 UNDO_LOG(前镜像/后镜像)
- 二阶段-成功:异步删除 UNDO_LOG
- 二阶段-回滚:用 UNDO_LOG 中的前镜像恢复数据
-- UNDO_LOG 表(自动创建)
CREATE TABLE undo_log (
branch_id BIGINT NOT NULL,
xid VARCHAR(128) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT NOT NULL,
log_created DATETIME(6) NOT NULL,
log_modified DATETIME(6) NOT NULL,
PRIMARY KEY (branch_id, xid)
);
7.3 Seata 四种模式对比
| 模式 | 原理 | 侵入性 | 性能 | 适用场景 |
|---|---|---|---|---|
| AT | 自动代理,记录 UNDO_LOG | 无 | 中 | 简单 CRUD,首推 |
| TCC | 手撕三阶段接口 | 高 | 高 | 复杂业务,性能要求高 |
| Saga | 状态机引擎 | 中 | 高 | 业务流程长 |
| XA | 标准 2PC | 无 | 低 | 强一致要求 |
8. 方案选型指南
事务类型
│
┌───────────────┼───────────────┐
│ │ │
短事务 长事务 最终一致
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────────┐
│ 2PC/XA │ │ Saga │ │ 消息+本地表 │
│ TCC │ │ 状态机 │ │ 最大努力通知 │
│ Seata-AT│ │ │ │ │
└─────────┘ └─────────┘ └─────────────┘
│ │ │
▼ ▼ ▼
强一致性 最终一致性 最终一致性
性能较低 吞吐高 吞吐最高
实现简单 实现复杂 实现中等
| 场景 | 推荐方案 |
|---|---|
| 单服务多数据源 | Seata AT / XA |
| 高并发短事务 | TCC |
| 长业务流程 | Saga |
| 跨异构系统 | 消息最终一致性 |
| 金融支付 | 2PC/XA + 对账 |
9. 总结
分布式事务没有银弹:
一致性 ←─────── 强度 ─────────→ 性能
│ │
2PC/XA ──► TCC ──► Saga ──► 消息最终一致性
│ │
强 弱/最终一致
│ │
低性能 高性能
工程实践建议:
- 能用单机事务就不用分布式事务(合理拆分避免跨服务事务)
- 能用最终一致性就不用强一致(消息 + 对账是最佳实践)
- 必须强一致时选择 Seata AT(低侵入、AT 模式足够覆盖 80% 场景)
- 监控补偿任务,确保悬挂事务最终完成
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。