引言
在分布式系统中,事务处理一直是一个复杂的话题。想象一下,当你在网上商城购物时,整个过程涉及:
- 订单系统创建订单
- 库存系统扣减库存
- 支付系统完成支付
- 积分系统增加积分
这些操作分布在不同的服务中,如何保证它们要么全部成功,要么全部失败?这就是分布式事务需要解决的问题。
分布式事务的挑战
传统事务的局限
在单体应用中,我们习惯使用数据库的 ACID 事务:
@Transactional
public void createOrder(Order order) {
// 创建订单
orderRepository.save(order);
// 扣减库存
inventoryRepository.deduct(order.getProductId(), order.getQuantity());
// 扣减余额
accountRepository.deduct(order.getUserId(), order.getAmount());
}
但在分布式环境下,这种方式行不通了,因为:
- 跨多个数据库
- 跨多个服务
- 网络可能失败
- 服务可能宕机
CAP 理论的限制
在分布式系统中,我们不得不在以下三个特性中做出选择:
- 一致性(Consistency)
- 可用性(Availability)
- 分区容错性(Partition tolerance)
TCC 模式介绍
什么是 TCC?
TCC(Try-Confirm-Cancel)是一种补偿性事务模式,它将一个完整的业务操作分为二步完成:
-
Try: 尝试执行业务
- 完成所有业务检查
- 预留必要的业务资源
-
Confirm: 确认执行业务
- 真正执行业务
- 不做任何业务检查
- 只使用 Try 阶段预留的资源
-
Cancel: 取消执行业务
- 释放 Try 阶段预留的资源
- 回滚操作
来源:seata
TCC 示例:订单支付流程
让我们通过一个具体的订单支付场景来理解 TCC:
// 订单服务的 TCC 实现
public class OrderTccService {
// Try: 创建预订单
@Transactional
public void tryCreate(Order order) {
// 检查订单参数
validateOrder(order);
// 创建预订单
order.setStatus(OrderStatus.TRYING);
orderRepository.save(order);
}
// Confirm: 确认订单
@Transactional
public void confirmCreate(String orderId) {
Order order = orderRepository.findById(orderId);
order.setStatus(OrderStatus.CONFIRMED);
orderRepository.save(order);
}
// Cancel: 取消订单
@Transactional
public void cancelCreate(String orderId) {
Order order = orderRepository.findById(orderId);
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}
// 库存服务的 TCC 实现
public class InventoryTccService {
// Try: 冻结库存
@Transactional
public void tryDeduct(String productId, int quantity) {
Inventory inventory = inventoryRepository.findById(productId);
// 检查并冻结库存
if (inventory.getAvailable() < quantity) {
throw new InsufficientInventoryException();
}
inventory.setFrozen(inventory.getFrozen() + quantity);
inventory.setAvailable(inventory.getAvailable() - quantity);
inventoryRepository.save(inventory);
}
// Confirm: 确认扣减
@Transactional
public void confirmDeduct(String productId, int quantity) {
Inventory inventory = inventoryRepository.findById(productId);
inventory.setFrozen(inventory.getFrozen() - quantity);
inventoryRepository.save(inventory);
}
// Cancel: 解冻库存
@Transactional
public void cancelDeduct(String productId, int quantity) {
Inventory inventory = inventoryRepository.findById(productId);
inventory.setFrozen(inventory.getFrozen() - quantity);
inventory.setAvailable(inventory.getAvailable() + quantity);
inventoryRepository.save(inventory);
}
}
// 支付服务的 TCC 实现
public class PaymentTccService {
// Try: 冻结金额
@Transactional
public void tryDeduct(String userId, BigDecimal amount) {
Account account = accountRepository.findById(userId);
// 检查并冻结金额
if (account.getAvailable().compareTo(amount) < 0) {
throw new InsufficientBalanceException();
}
account.setFrozen(account.getFrozen().add(amount));
account.setAvailable(account.getAvailable().subtract(amount));
accountRepository.save(account);
}
// Confirm: 确认支付
@Transactional
public void confirmDeduct(String userId, BigDecimal amount) {
Account account = accountRepository.findById(userId);
account.setFrozen(account.getFrozen().subtract(amount));
accountRepository.save(account);
}
// Cancel: 解冻金额
@Transactional
public void cancelDeduct(String userId, BigDecimal amount) {
Account account = accountRepository.findById(userId);
account.setFrozen(account.getFrozen().subtract(amount));
account.setAvailable(account.getAvailable().add(amount));
accountRepository.save(account);
}
}
TCC 事务协调器
为了协调整个 TCC 流程,我们需要一个事务协调器:
@Service
public class OrderTccCoordinator {
@Autowired
private OrderTccService orderService;
@Autowired
private InventoryTccService inventoryService;
@Autowired
private PaymentTccService paymentService;
public void createOrder(Order order) {
String xid = generateTransactionId();
try {
// ==== Try 阶段 ====
// 1. 创建预订单
orderService.tryCreate(order);
// 2. 尝试扣减库存
inventoryService.tryDeduct(
order.getProductId(),
order.getQuantity()
);
// 3. 尝试扣减余额
paymentService.tryDeduct(
order.getUserId(),
order.getAmount()
);
// ==== Confirm 阶段 ====
// 1. 确认订单
orderService.confirmCreate(order.getId());
// 2. 确认库存扣减
inventoryService.confirmDeduct(
order.getProductId(),
order.getQuantity()
);
// 3. 确认支付
paymentService.confirmDeduct(
order.getUserId(),
order.getAmount()
);
} catch (Exception e) {
// ==== Cancel 阶段 ====
// 1. 取消订单
orderService.cancelCreate(order.getId());
// 2. 恢复库存
inventoryService.cancelDeduct(
order.getProductId(),
order.getQuantity()
);
// 3. 恢复余额
paymentService.cancelDeduct(
order.getUserId(),
order.getAmount()
);
throw new OrderCreateFailedException(e);
}
}
}
TCC 实现要点
1. 业务模型设计
在实现 TCC 时,业务模型需要考虑预留资源的状态:
public class Inventory {
private String productId;
private int total; // 总库存
private int available; // 可用库存
private int frozen; // 冻结库存
}
public class Account {
private String userId;
private BigDecimal total; // 总额
private BigDecimal available; // 可用余额
private BigDecimal frozen; // 冻结金额
}
图 3: TCC 中的资源状态变化,来源 seata
2. 幂等性设计
所有操作都需要保证幂等,因为在网络异常时可能会重试:
@Transactional
public void tryDeduct(String userId, BigDecimal amount, String xid) {
// 检查是否已经执行过
if (tccLogRepository.existsByXidAndPhase(xid, "try")) {
return;
}
// 执行业务逻辑
Account account = accountRepository.findById(userId);
account.setFrozen(account.getFrozen().add(amount));
account.setAvailable(account.getAvailable().subtract(amount));
accountRepository.save(account);
// 记录执行日志
tccLogRepository.save(new TccLog(xid, "try"));
}
3. 防悬挂设计
为什么需要防悬挂?
在分布式系统中,网络延迟、服务故障等原因可能导致一个奇怪的现象,Cancel 操作比 Try 操作先执行。这就是所谓的"悬挂"问题。具体场景如下:
事务管理器在调用 TCC 服务的一阶段 Try 操作时事务时,由于网络拥堵,Try 请求没有及时到达,事务管理器超时后,发起了 Cancel 请求完成后,此时原来的 Try 请求才到达,如果在执行这个延迟的 Try 请求,将导致资源被错误锁定
*图: TCC 悬挂问题示意图,来源:seata
解决方案
核心思路是记录每个事务的执行状态,并在执行 Try 操作前进行检查:
@Service
public class TccTransactionService {
@Autowired
private TccLogRepository tccLogRepository;
@Transactional
public void tryDeduct(String userId, BigDecimal amount, String xid) {
// 1. 检查是否已经被 Cancel
if (tccLogRepository.existsByXidAndPhase(xid, "cancel")) {
throw new TransactionCancelledException("Transaction already cancelled");
}
// 2. 检查是否已经执行过 Try (幂等性检查)
if (tccLogRepository.existsByXidAndPhase(xid, "try")) {
return;
}
// 3. 执行业务逻辑
Account account = accountRepository.findById(userId);
if (account.getAvailable().compareTo(amount) < 0) {
throw new InsufficientBalanceException();
}
// 4. 记录执行日志
account.setFrozen(account.getFrozen().add(amount));
account.setAvailable(account.getAvailable().subtract(amount));
accountRepository.save(account);
tccLogRepository.save(new TccLog(xid, "try"));
}
}
4. 超时处理
为什么需要超时处理?
在分布式环境下,超时是不可避免的,可能由于以下原因导致:
- 网络延迟或故障
- 服务器负载过高
- 服务进程崩溃
- 死锁
如果不处理超时,会造成严重后果:
- 资源被无限期锁定
- 事务无法正常结束
- 系统可用性降低
- 用户体验变差
超时处理机制
- 定时扫描超时事务
@Component
public class TccTimeoutChecker {
@Autowired
private TccLogRepository tccLogRepository;
@Autowired
private TccTransactionHandler transactionHandler;
@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void checkTimeout() {
// 1. 查找超时的事务
List<TccLog> timeoutLogs = tccLogRepository
.findByPhaseAndCreateTimeBefore(
"try",
LocalDateTime.now().minusMinutes(5)
);
for (TccLog log : timeoutLogs) {
try {
// 2. 执行 Cancel 操作
transactionHandler.cancelTransaction(log.getXid());
// 3. 记录取消日志
log.setPhase("cancel");
log.setUpdateTime(LocalDateTime.now());
tccLogRepository.save(log);
} catch (Exception e) {
// 4. 记录错误,可能需要人工介入
errorLogger.log(
"Failed to cancel timeout transaction: " + log.getXid(),
e
);
}
}
}
}
- 超时配置管理
@Configuration
public class TccConfig {
@Value("${tcc.transaction.timeout:60000}")
private long transactionTimeout; // 默认60秒
@Value("${tcc.check.interval:5000}")
private long checkInterval; // 默认5秒
@Value("${tcc.retry.max:3}")
private int maxRetryCount; // 默认重试3次
@Value("${tcc.retry.interval:1000}")
private long retryInterval; // 默认重试间隔1秒
// getter and setter
}
- 监控和告警
@Component
public class TccMonitor {
@Autowired
private AlertService alertService;
public void onTransactionTimeout(String xid) {
// 记录监控指标
MetricsRegistry.counter("tcc.timeout").increment();
// 发送告警
alertService.sendAlert(
"TCC Transaction Timeout",
String.format("Transaction %s timeout", xid),
AlertLevel.WARNING
);
}
public void onCancelFailed(String xid, Exception e) {
// 记录监控指标
MetricsRegistry.counter("tcc.cancel.failed").increment();
// 发送告警
alertService.sendAlert(
"TCC Cancel Failed",
String.format("Transaction %s cancel failed: %s", xid, e.getMessage()),
AlertLevel.ERROR
);
}
}
最佳实践
-
超时时间设置
- 根据业务特点设置合理的超时时间
- 考虑网络延迟和服务响应时间
- 为复杂业务预留足够的处理时间
- 不同类型的事务可以设置不同的超时时间
-
重试机制
- 实现指数退避算法
- 设置最大重试次数
- 合理的重试间隔
- 重试时要考虑幂等性
-
监控和告警
- 监控超时事务数量
- 监控 Cancel 操作的成功率
- 监控资源占用情况
- 设置合理的告警阈值
-
人工干预
- 提供管理后台
- 支持手动触发 Cancel
- 提供事务状态查询
- 记录详细的操作日志
通过这些机制的组合,我们可以构建一个健壮的 TCC 事务处理系统,能够:
- 及时发现并处理超时事务
- 防止资源被长期锁定
- 提供完善的监控和运维能力
- 在出现问题时及时告警并支持人工介入
最佳实践
-
资源预留
- Try 阶段要预留足够的资源
- 预留资源要考虑并发情况
- 预留时间要合理设置
-
状态机制
- 明确定义每个阶段的状态
- 状态转换要有清晰的规则
- 保存状态转换历史
-
异常处理
- 所有异常都要有补偿措施
- 补偿操作要能重试
- 重试策略要合理设置
-
监控告警
- 监控每个阶段的执行情况
- 设置合理的告警阈值
- 提供人工干预的接口
适用场景
TCC 模式适合:
- 强一致性要求高的业务
- 实时性要求高的场景
- 有资源锁定需求的操作
不适合:
- 业务逻辑简单的场景
- 对性能要求特别高的场景
- 补偿成本过高的业务
结论
TCC 是一种强大的分布式事务解决方案,它通过巧妙的补偿机制来保证事务的一致性。虽然实现较为复杂,但在某些场景下是不可替代的选择。
关键是要:
- 理解业务场景
- 合理设计补偿逻辑
- 做好异常处理
- 重视监控告警
通过合理使用 TCC 模式,我们可以在分布式系统中实现可靠的事务处理。
文章评论