在微服务架构中,一次业务操作往往跨越多个服务和数据库,传统单机事务的 ACID 保证无法直接适用。分布式事务成为企业级系统必须面对的复杂课题。
一、分布式事务的核心挑战
1.1 CAP 与 BASE 理论
| 理论 | 核心观点 | 对事务的影响 |
|---|---|---|
| CAP | 一致性、可用性、分区容错性不可兼得 | 通常选择 AP,牺牲强一致性 |
| BASE | 基本可用、软状态、最终一致 | 接受短暂不一致,追求最终一致 |
1.2 分布式事务 vs 本地事务
// 本地事务:简单直接
@Transactional
public void localOrder() {
orderDao.save(order); // DB1
inventoryDao.deduct(sku); // 同一DB
}
// 分布式事务:跨服务、跨数据库
public void distributedOrder() {
orderService.createOrder(order); // svc1 + DB1
inventoryService.deduct(sku, qty); // svc2 + DB2
paymentService.charge(userId, amt); // svc3 + DB3
}
二、Seata:开源分布式事务解决方案
2.1 Seata 架构组件
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ TM (事务管理器) │----│ RM (资源管理器) │----│ TC (协调中心) │
│ @GlobalTransactional │ │ 各服务的 DataSourceProxy │ │ 维护全局事务状态│
└──────────────┘ └──────────────┘ └──────────────┘
2.2 AT 模式(Automatic Transaction)
AT 模式基于 SQL 解析和反向补偿,对业务零侵入。
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public Order createOrder(OrderRequest req) {
// 1. 创建订单(DB1)
Order order = orderService.create(req);
// 2. 扣减库存(远程调用 svc2)
inventoryService.deduct(req.getSkuId(), req.getQuantity());
// 3. 扣减余额(远程调用 svc3)
paymentService.debit(req.getUserId(), req.getAmount());
return order;
}
AT 模式原理:
- 一阶段:RM 拦截并解析 SQL,在业务 SQL 执行前后记录数据快照(before image / after image)到
undo_log表。 - 二阶段-提交:若全局事务成功,TC 通知各 RM 异步删除
undo_log。 - 二阶段-回滚:若全局事务失败,TC 通知各 RM 根据
undo_log执行反向 SQL 恢复数据。
-- undo_log 表结构(关键字段)
SELECT * FROM undo_log WHERE xid = 'xxx';
-- branch_id | xid | context | rollback_info(JSON格式快照)
AT 模式优缺点:
| 优点 | 缺点 |
|---|---|
| 业务零侵入 | 并非所有 SQL 都支持(如批量更新复杂 join) |
| 自动回滚补偿 | 需要额外存储 before/after image |
| 性能损耗较低 | 一阶段锁定资源时间较长 |
2.3 TCC 模式(Try-Confirm-Cancel)
TCC 将业务操作拆分为三个阶段,需要业务层实现三个接口。
public interface InventoryTccAction {
@TwoPhaseBusinessAction(name = "inventoryDeduct")
boolean tryDeduct(@BusinessActionContextParameter(paramName = "skuId") String skuId,
@BusinessActionContextParameter(paramName = "qty") int qty);
boolean commit(BusinessActionContext ctx);
boolean rollback(BusinessActionContext ctx);
}
@Service
public class InventoryTccActionImpl implements InventoryTccAction {
@Autowired
private InventoryDao inventoryDao;
@Override
public boolean tryDeduct(String skuId, int qty) {
// 检查库存并预留(冻结)
return inventoryDao.freezeStock(skuId, qty) > 0;
}
@Override
public boolean commit(BusinessActionContext ctx) {
String skuId = ctx.getActionContext("skuId");
int qty = Integer.parseInt(ctx.getActionContext("qty"));
// 确认扣减:将冻结库存转为实际扣减
inventoryDao.confirmDeduct(skuId, qty);
return true;
}
@Override
public boolean rollback(BusinessActionContext ctx) {
String skuId = ctx.getActionContext("skuId");
int qty = Integer.parseInt(ctx.getActionContext("qty"));
// 释放冻结库存
inventoryDao.unfreezeStock(skuId, qty);
return true;
}
}
TCC 注意事项:
- 幂等性:Confirm 和 Cancel 需保证幂等(网络超时可能重复调用)。
- 空回滚:Try 未执行时,Cancel 不应报错(需记录事务状态)。
- 悬挂:Cancel 先于 Try 执行,Try 需识别并跳过。
2.4 Saga 模式
Saga 将长事务拆分为一系列本地事务,每个本地事务提交后立即释放资源。
// 正向流程
public class OrderSaga {
@Autowired private OrderService orderService;
@Autowired private InventoryService inventoryService;
@Autowired private PaymentService paymentService;
public void start(OrderRequest req) {
// Step 1: 创建订单
orderService.create(req);
// Step 2: 扣减库存
inventoryService.deduct(req.getSkuId(), req.getQuantity());
// Step 3: 扣款
paymentService.charge(req.getUserId(), req.getAmount());
}
// 补偿流程(逆序执行)
public void compensate(String orderId, String skuId, Long userId) {
paymentService.refund(orderId); // 退款
inventoryService.restore(skuId); // 恢复库存
orderService.cancel(orderId); // 取消订单
}
}
Saga 的两种实现方式:
| 类型 | 说明 | 适用场景 |
|---|---|---|
| 编排式 Saga(Choreography) | 各服务完成本地事务后发送事件,驱动下一个服务 | 简单流程,服务间松耦合 |
| 编排式 Saga(Orchestration) | 中央协调器统一调度正向/补偿流程 | 复杂流程,需要集中管控 |
三、本地消息表:最终一致性方案
本地消息表是业务侵入最小的最终一致性方案,核心思想是将分布式事务转化为本地事务 + 异步消息。
@Transactional
public void createOrder(OrderRequest req) {
// 1. 保存订单
orderDao.save(req.toOrder());
// 2. 写入消息表(同一本地事务)
Message message = Message.builder()
.topic("order_created")
.body(JsonUtils.toJson(req))
.status("PENDING")
.retryCount(0)
.build();
messageDao.save(message);
}
// 定时任务扫描消息表
@Scheduled(fixedRate = 5000)
public void scanPendingMessages() {
List<Message> pending = messageDao.findByStatus("PENDING");
for (Message msg : pending) {
try {
kafkaTemplate.send(msg.getTopic(), msg.getBody());
msg.setStatus("SENT");
messageDao.update(msg);
} catch (Exception e) {
msg.setRetryCount(msg.getRetryCount() + 1);
if (msg.getRetryCount() > 3) {
msg.setStatus("DEAD"); // 进入死信队列
}
messageDao.update(msg);
}
}
}
四、事务模式选型指南
| 模式 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| Seata AT | 强一致 | 中 | 低 | 简单 CRUD,无复杂 SQL |
| Seata TCC | 强一致 | 高 | 高 | 高并发,需快速释放资源 |
| Saga | 最终一致 | 高 | 中 | 长事务,业务流程复杂 |
| 本地消息表 | 最终一致 | 高 | 低 | 异步场景,容忍延迟 |
| 最大努力通知 | 最终一致 | 高 | 低 | 对账场景,可人工介入 |
五、常见问题与最佳实践
5.1 避免分布式事务
最好的分布式事务是不使用分布式事务。
- 服务拆分粒度合理:避免频繁跨服务事务。
- 业务补偿替代事务:如发货失败触发退款流程。
- 异步化:非核心操作改为消息异步处理。
5.2 监控与告警
# Seata TC 监控指标
seata:
metrics:
enabled: true
registry-type: prometheus
exporter-list: prometheus
exporter-prometheus-port: 9898
5.3 超时与重试
@GlobalTransactional(
name = "create-order",
timeoutMills = 300000, // 5分钟超时
retryTimes = 3 // 重试次数
)
六、总结
| 方面 | 建议 |
|---|---|
| 简单场景 | Seata AT 模式,低侵入 |
| 高性能场景 | TCC 模式,资源锁定时间短 |
| 长事务/工作流 | Saga 模式,分阶段提交 |
| 异步通知 | 本地消息表,可靠性高 |
| 核心原则 | 能不用就不用,避免过度设计 |
分布式事务没有银弹,理解业务场景的本质需求,选择合适的一致性级别,才是工程实践中的最佳策略。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。