分布式事务解决方案全景:从 2PC 到 Saga,每种方案的适用场景与落地要点
分布式事务是后端架构最难的问题之一。很多团队的方案就是"不管他,出了问题手动补",这在业务量小的时候还行,一旦上了规模就会成为定时炸弹。本文系统梳理分布式事务的六大解决方案,配合真实代码示例,帮你根据业务场景选对方案,不走弯路。
一、分布式事务的根本难题
为什么本地事务解决不了分布式场景?
单库事务(本地事务):
BEGIN
UPDATE order SET status='PAID' WHERE id=1001; -- 订单库
UPDATE inventory SET stock=stock-1 WHERE id=501; -- 同一个库
COMMIT -- ACID 保障,要么全成功要么全回滚
跨库/跨服务场景(分布式事务困境):
订单服务:BEGIN → UPDATE order → COMMIT ✅
库存服务:BEGIN → UPDATE inventory → COMMIT ✅ or ❌
支付服务:BEGIN → INSERT payment → COMMIT ✅ or ❌
问题:三个服务分别提交,网络/机器故障导致部分成功、部分失败
→ 数据不一致!
CAP 理论的本质约束
在网络分区(P)不可避免的情况下,C(强一致性)和 A(高可用)二选一:
- 强一致性方案(CP):2PC、TCC → 性能开销大,可用性受损
- 最终一致性方案(AP):Saga、可靠消息 → 性能好,暂时不一致可接受
二、六大分布式事务方案全景
方案1:XA/2PC(两阶段提交)
Phase 1(准备阶段):
协调者 → 参与者1:prepare? → 参与者1:ready ✅
协调者 → 参与者2:prepare? → 参与者2:ready ✅
协调者 → 参与者3:prepare? → 参与者3:ready ✅
Phase 2(提交阶段):
协调者 → 参与者1:commit!
协调者 → 参与者2:commit!
协调者 → 参与者3:commit!
Java 实现(Atomikos XA):
java
@Configuration
public class XADataSourceConfig {
@Bean
@Primary
public DataSource orderXADataSource() {
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setUniqueResourceName("orderDataSource");
ds.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
Properties p = new Properties();
p.setProperty("URL", "jdbc:mysql://order-db:3306/order");
p.setProperty("user", "root");
p.setProperty("password", "${ORDER_DB_PASS}");
ds.setXaProperties(p);
ds.setPoolSize(20);
return ds;
}
// 库存数据源同理
}
// 业务代码:Spring 自动管理跨库事务
@Transactional // Atomikos JTA 事务管理器自动处理跨库提交
public void createOrder(OrderDTO dto) {
orderRepository.save(dto.toOrder()); // 写订单库
inventoryRepository.deduct(dto.itemId()); // 写库存库(同一事务)
}
适用场景: 同一机房内的异构数据库跨库操作
不适合: 跨服务调用(性能差,协调者故障导致全局阻塞)
方案2:TCC(Try-Confirm-Cancel)
TCC 将一次业务操作拆成三个阶段:
Try: 预留资源(冻结库存、冻结余额)
Confirm:确认提交(实际扣减库存、实际扣款)
Cancel: 取消回滚(释放冻结库存、退还冻结余额)
实现示例(Seata TCC):
java
// 库存服务:TCC 接口
@LocalTCC
public interface InventoryTccAction {
@TwoPhaseBusinessAction(name = "deductInventory",
commitMethod = "confirm", rollbackMethod = "cancel")
boolean tryDeduct(@BusinessActionContextParameter(paramName = "itemId") Long itemId,
@BusinessActionContextParameter(paramName = "qty") Integer qty,
BusinessActionContext context);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
@Component
public class InventoryTccActionImpl implements InventoryTccAction {
@Override
@Transactional
public boolean tryDeduct(Long itemId, Integer qty, BusinessActionContext context) {
// Try 阶段:冻结库存(不实际扣减)
Inventory inv = inventoryRepository.findById(itemId);
if (inv.getAvailable() < qty) {
return false; // 库存不足,Try 失败
}
// 冻结库存
inv.setFrozen(inv.getFrozen() + qty);
inv.setAvailable(inv.getAvailable() - qty);
inventoryRepository.save(inv);
return true;
}
@Override
@Transactional
public boolean confirm(BusinessActionContext context) {
// Confirm 阶段:将冻结库存转为实际扣减
Long itemId = Long.valueOf(context.getActionContext("itemId").toString());
Integer qty = Integer.valueOf(context.getActionContext("qty").toString());
Inventory inv = inventoryRepository.findById(itemId);
inv.setFrozen(inv.getFrozen() - qty); // 解冻
// 库存已在 Try 阶段预扣,Confirm 只需解冻
inventoryRepository.save(inv);
return true;
}
@Override
@Transactional
public boolean cancel(BusinessActionContext context) {
// Cancel 阶段:释放冻结库存
Long itemId = Long.valueOf(context.getActionContext("itemId").toString());
Integer qty = Integer.valueOf(context.getActionContext("qty").toString());
Inventory inv = inventoryRepository.findById(itemId);
inv.setFrozen(inv.getFrozen() - qty);
inv.setAvailable(inv.getAvailable() + qty); // 退还
inventoryRepository.save(inv);
return true;
}
}
TCC 三个必须处理的坑:
java
// 坑1:空回滚(Try 未执行,直接收到 Cancel)
// 解决:Cancel 前检查是否有 Try 记录,无记录则直接返回成功
public boolean cancel(BusinessActionContext context) {
String xid = context.getXid();
if (!tryRecordRepository.exists(xid)) {
log.warn("空回滚,Try 记录不存在,xid: {}", xid);
return true; // 幂等返回
}
// 正常回滚逻辑...
}
// 坑2:幂等(Confirm/Cancel 可能被重复调用)
// 解决:记录事务状态,重复调用直接返回
public boolean confirm(BusinessActionContext context) {
String xid = context.getXid();
TccRecord record = tccRecordRepository.findByXid(xid);
if (record != null && record.getStatus() == CONFIRMED) {
return true; // 已提交,幂等返回
}
// 执行 Confirm 逻辑...
}
// 坑3:悬挂(Cancel 先于 Try 执行)
// 解决:收到 Cancel 后记录,后续 Try 到来时拒绝
public boolean tryDeduct(Long itemId, Integer qty, BusinessActionContext context) {
String xid = context.getXid();
if (cancelRecordRepository.exists(xid)) {
log.warn("悬挂,Cancel 已执行,拒绝 Try,xid: {}", xid);
return false;
}
// 正常 Try 逻辑...
}
方案3:Saga 模式(长事务首选)
Saga 将长事务拆分成多个本地事务的序列,每个步骤有对应的补偿操作:
正向流程:
T1(创建订单)→ T2(冻结库存)→ T3(发起支付)→ T4(扣减库存)→ T5(发货通知)
补偿流程(T3 失败时):
C2(释放库存)← C1(取消订单)
Seata Saga State Machine 配置(JSON):
json
{
"Name": "orderFulfillmentStateMachine",
"Comment": "订单履约 Saga",
"StartState": "CreateOrder",
"Version": "0.0.1",
"States": {
"CreateOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "createOrder",
"CompensateState": "CompensateCreateOrder",
"Next": "FreezeInventory",
"Output": { "orderId": "$.#root" }
},
"FreezeInventory": {
"Type": "ServiceTask",
"ServiceName": "inventoryService",
"ServiceMethod": "freezeInventory",
"CompensateState": "CompensateFreezeInventory",
"Input": [{ "orderId": "$.orderId" }],
"Next": "MakePayment",
"Catch": [
{
"Exceptions": ["com.example.InsufficientInventoryException"],
"Next": "CompensateCreateOrder"
}
]
},
"MakePayment": {
"Type": "ServiceTask",
"ServiceName": "paymentService",
"ServiceMethod": "makePayment",
"CompensateState": "CompensateMakePayment",
"Next": "Succeed"
},
"CompensateCreateOrder": {
"Type": "ServiceTask",
"ServiceName": "orderService",
"ServiceMethod": "cancelOrder",
"Next": "Fail"
},
"CompensateFreezeInventory": {
"Type": "ServiceTask",
"ServiceName": "inventoryService",
"ServiceMethod": "releaseInventory",
"Next": "CompensateCreateOrder"
}
}
}
方案4:可靠消息最终一致性
java
// 下单时:将消息与本地事务绑定发送(Transactional Outbox 模式)
@Transactional
public void createOrder(OrderDTO dto) {
// 1. 写订单
Order order = orderRepository.save(dto.toOrder());
// 2. 写消息发件箱(与订单同一个事务!)
OutboxMessage msg = OutboxMessage.builder()
.aggregateId(order.getId().toString())
.aggregateType("Order")
.eventType("OrderCreated")
.payload(JSON.toJSONString(new OrderCreatedEvent(order)))
.status(PENDING)
.createdAt(LocalDateTime.now())
.build();
outboxRepository.save(msg);
// 本地事务提交:订单 + 消息同时成功 or 同时失败
}
// 消息中继器:独立轮询发件箱,发送到 MQ
@Scheduled(fixedDelay = 100)
public void relayMessages() {
List<OutboxMessage> pending = outboxRepository.findPending(100);
for (OutboxMessage msg : pending) {
try {
kafkaProducer.send(new ProducerRecord<>("domain-events",
msg.getAggregateId(), msg.getPayload()));
msg.setStatus(SENT);
outboxRepository.save(msg);
} catch (Exception e) {
log.error("消息发送失败,将重试", e);
}
}
}
// 库存服务消费:幂等处理
@KafkaListener(topics = "domain-events")
public void handleOrderCreated(String payload) {
OrderCreatedEvent event = JSON.parseObject(payload, OrderCreatedEvent.class);
// 幂等检查
if (processedEventRepository.exists(event.getEventId())) {
log.info("重复消息,忽略,eventId: {}", event.getEventId());
return;
}
// 处理业务逻辑
inventoryService.deductInventory(event.getItemId(), event.getQty());
// 标记已处理
processedEventRepository.save(event.getEventId());
}
三、方案选型矩阵
| 方案 | 一致性 | 性能 | 开发复杂度 | 适用场景 |
|---|---|---|---|---|
| XA/2PC | 强一致 | ★★ | 低 | 同机房异构数据库,低并发 |
| TCC | 最终一致(极短暂不一致) | ★★★★ | 高 | 金融/支付,不允许中间状态暴露 |
| Saga | 最终一致 | ★★★★ | 中 | 长流程业务(订单履约、退款流程) |
| 可靠消息 | 最终一致 | ★★★★★ | 低 | 跨服务异步解耦,允许短暂不一致 |
| 最大努力通知 | 最终一致 | ★★★★★ | 最低 | 非核心跨系统通知(短信/积分) |
| AT 模式(Seata) | 最终一致 | ★★★ | 最低 | 快速落地,业务代码改动最少 |
四、Seata AT 模式:最快落地方案
yaml
# application.yml - Seata 配置
seata:
enabled: true
application-id: order-service
tx-service-group: order-tx-group
registry:
type: nacos
nacos:
server-addr: nacos:8848
namespace: seata
config:
type: nacos
java
// 使用 @GlobalTransactional 开启分布式事务
// AT 模式:Seata 自动拦截 SQL,生成 undo_log,无需手写补偿逻辑
@GlobalTransactional(timeoutMills = 30000, name = "create-order-tx")
public void createOrder(OrderDTO dto) {
// 调用订单服务(本地事务)
orderService.createOrder(dto);
// 调用库存服务(RPC,Seata 自动协调)
inventoryService.deductStock(dto.getItemId(), dto.getQty());
// 调用支付服务(RPC)
paymentService.createPayment(dto.getUserId(), dto.getAmount());
// 任何一步失败,Seata 自动回滚所有参与方
}
五、痛点与避坑指南
坑1:Saga 补偿逻辑漏写
每个正向步骤必须有对应的补偿步骤,且补偿操作必须是幂等的。建议用状态机工具(Seata Saga)强制管理,不要手写。
坑2:TCC 忘记处理空回滚和幂等
这是 TCC 最常见的 BUG,见上文代码示例,三个反模式必须全部处理。
坑3:Seata AT 模式在高并发下锁竞争
AT 模式对热点数据会有行锁竞争。高并发场景(秒杀、抢购)建议改用 TCC + Redis 热点数据处理。
坑4:可靠消息丢失导致数据不一致
必须用 Transactional Outbox 模式,消息与本地数据同事务写入,不能先写 DB 再发 MQ(中间可能崩溃)。
六、全文总结
分布式事务方案选型核心决策树:
是否强一致性需求?
├── 是 → 同机房 → XA/2PC;跨机房 → TCC
└── 否(最终一致可接受)→
有长业务流程需要补偿?→ 是 → Saga
纯异步解耦场景?→ 是 → 可靠消息
快速落地,改造最少?→ Seata AT 模式
七、行业技术展望
- Seata 2.x 版本:支持更多数据库驱动,AT 模式性能优化显著
- 基于 AI 的异常事务检测:自动识别长时间未完成的 Saga 并触发告警
- XA over 云数据库:阿里云 PolarDB、腾讯云 TDSQL 已原生支持跨实例 XA 事务
参考文献
- Seata 官方文档 - https://seata.apache.org/zh-cn/docs/overview/what-is-seata
- Martin Fowler - Saga Pattern - https://microservices.io/patterns/data/saga.html
- Transactional Outbox Pattern - https://microservices.io/patterns/data/transactional-outbox.html
- 阿里云 Seata 最佳实践 - https://help.aliyun.com/zh/mse/use-cases/seata-best-practices
- Chris Richardson - Microservices Patterns - https://microservices.io/
- Microsoft Azure 分布式事务指南 - https://learn.microsoft.com/zh-cn/azure/architecture/patterns/
- 《分布式系统:概念与设计》(第5版)George Coulouris 著
- 腾讯云 DTF(分布式事务框架)文档 - https://cloud.tencent.com/document/product/1291