一、我与TCC的"相爱相杀"
第一次接触TCC模式,是在一个支付系统中。
当时的场景是这样的:用户发起一笔转账,从账户A转100块到账户B。传统方式:
账户A:balance -= 100
账户B:balance += 100
这两个操作在不同服务里,网络抖动、服务器宕机......各种情况都可能导致数据不一致。
我当时的方案是:写一条"转账记录"到消息表,用定时任务补偿。这个方案勉强能用,但:
- 定时任务延迟大,用户可能等很久
- 补偿逻辑复杂,容易出错
- 很难保证"恰好一次"
后来我接触到了TCC模式,才发现这才是正确的打开方式。但TCC的概念看起来简单,真正落地时遇到的坑,让我差点放弃:
- Try阶段的资源冻结时机不对
- Confirm和Cancel的幂等没做好
- 空回滚问题困扰了很久
- 悬挂问题差点让我多扣了用户的钱
今天这篇文章,是我踩坑踩出来的经验总结,希望能帮你少走弯路。
二、TCC原理:理解两阶段提交
2.1 TCC的三阶段
TCC = Try + Confirm + Cancel,这三个阶段对应分布式事务的生命周期:
TCC事务生命周期:
┌─────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 第一阶段:Try(预留资源) │ │
│ │ │ │
│ │ 账户A:冻结100元(balance_frozen += 100) │ │
│ │ 账户B:预增加100元(balance_pending += 100) │ │
│ │ │ │
│ │ 特点: │ │
│ │ - 只预留资源,不真正扣减 │ │
│ │ - 失败了也不会有实际损失 │ │
│ │ - 所有参与者的Try都要成功 │ │
│ └─────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 第二阶段:Confirm(确认执行) │ │
│ │ │ │
│ │ 账户A:真正扣减(balance -= 100, frozen -= 100) │ │
│ │ 账户B:真正增加(balance += 100, pending -= 100)│ │
│ │ │ │
│ │ 特点: │ │
│ │ - 使用Try阶段预留的资源 │ │
│ │ - 必须成功,不能失败 │ │
│ └─────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────┐ │
│ │ Confirm失败/超时 │ │
│ └───────────┬───────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 第三阶段:Cancel(回滚) │ │
│ │ │ │
│ │ 账户A:解冻(frozen -= 100) │ │
│ │ 账户B:回退(pending -= 100) │ │
│ │ │ │
│ │ 特点: │ │
│ │ - 释放Try阶段预留的资源 │ │
│ │ - Confirm失败也会触发Cancel │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
2.2 TCC与2PC的区别
| 维度 | 2PC(两阶段提交) | TCC |
|---|---|---|
| 协调者 | 数据库协调者 | 应用协调者(TC) |
| 执行位置 | 数据库层面 | 应用层面 |
| 资源锁定 | 锁定数据行 | 预留/冻结资源 |
| 阻塞 | 锁定期间阻塞 | 不阻塞其他事务 |
| 回滚 | 数据库回滚 | 业务补偿 |
| 性能 | 较差 | 较好 |
2.3 为什么需要TCC
场景:转账100元,账户A和账户B在不同的服务
不用TCC:
账户A扣100元成功
账户B加100元失败(网络超时)
结果:钱丢了!
用TCC:
Try阶段:账户A冻结100元,账户B预增加100元(都成功)
Confirm阶段:账户A真正扣,账户B真正加(都成功)
或者:
Try阶段:账户A冻结100元,账户B预增加100元(账户B超时失败)
Cancel阶段:账户A解冻100元(回滚)
三、TCC实现:代码层面的细节
3.1 TCC接口定义
java
// TCC接口需要标注@LocalTCC
@LocalTCC
public interface TransferTccService {
/**
* Try阶段:预留资源
* @param actionContext 事务上下文,Seata自动传递
* @param fromAccount 付款账户
* @param toAccount 收款账户
* @param amount 金额
*/
@TwoPhaseBusinessAction(
name = "transferAction",
commitMethod = "confirm",
rollbackMethod = "cancel",
tryTimeout = 30000 // Try超时时间(毫秒)
)
boolean tryTransfer(
BusinessActionContext actionContext,
@BusinessActionContextParameter(paramName = "fromAccount") String fromAccount,
@BusinessActionContextParameter(paramName = "toAccount") String toAccount,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount
);
/**
* Confirm阶段:确认执行
* 必须幂等,失败会重试
*/
boolean confirm(BusinessActionContext actionContext);
/**
* Cancel阶段:回滚
* 必须幂等,失败会重试
*/
boolean cancel(BusinessActionContext actionContext);
}
3.2 业务实现:Try阶段
java
@Service
public class TransferTccServiceImpl implements TransferTccService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private TransferLogMapper transferLogMapper;
@Override
@Transactional
public boolean tryTransfer(
BusinessActionContext context,
String fromAccount,
String toAccount,
BigDecimal amount) {
// 获取全局事务ID(用于幂等)
String xid = context.getXid();
String fromKey = fromAccount + "_" + xid;
// ========== 1. 检查幂等 ==========
// 如果已经有转账记录,说明是重复执行
if (transferLogMapper.existsByXidAndAction(fromKey, "try")) {
log.info("Try阶段已执行过,幂等跳过: xid={}", xid);
return true;
}
// ========== 2. 检查账户A余额 ==========
Account fromAccountEntity = accountMapper.selectByAccountNo(fromAccount);
if (fromAccountEntity == null) {
throw new RuntimeException("付款账户不存在");
}
// ========== 3. 检查余额是否足够 ==========
if (fromAccountEntity.getAvailableBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
// ========== 4. 冻结金额 ==========
// 冻结金额 = 原冻结 + 本次转账金额
BigDecimal newFrozenBalance =
fromAccountEntity.getFrozenBalance().add(amount);
accountMapper.updateFrozenBalance(fromAccount, newFrozenBalance);
// ========== 5. 记录转账日志(幂等标记) ==========
TransferLog log = new TransferLog();
log.setXid(xid);
log.setActionKey(fromKey);
log.setAction("try");
log.setFromAccount(fromAccount);
log.setToAccount(toAccount);
log.setAmount(amount);
log.setStatus("PENDING"); // 待确认
log.setCreateTime(new Date());
transferLogMapper.insert(log);
log.info("Try阶段完成:冻结{}元 from={}, xid={}", amount, fromAccount, xid);
return true;
}
}
3.3 业务实现:Confirm阶段
java
@Override
public boolean confirm(BusinessActionContext context) {
String xid = context.getXid();
String fromAccount = (String) context.getActionContext("fromAccount");
String toAccount = (String) context.getActionContext("toAccount");
BigDecimal amount = new BigDecimal(
context.getActionContext("amount").toString()
);
String fromKey = fromAccount + "_" + xid;
// ========== 1. 检查幂等 ==========
// 如果Confirm已执行,跳过
if (transferLogMapper.existsByXidAndAction(fromKey, "confirm")) {
log.info("Confirm阶段已执行过,幂等跳过: xid={}", xid);
return true;
}
try {
// ========== 2. 从冻结转为真正扣减 ==========
Account fromEntity = accountMapper.selectByAccountNo(fromAccount);
Account toEntity = accountMapper.selectByAccountNo(toAccount);
// 扣减可用余额(恢复冻结,转入冻结)
BigDecimal newFrozenBalance = fromEntity.getFrozenBalance().subtract(amount);
fromEntity.setFrozenBalance(newFrozenBalance);
fromEntity.setAvailableBalance(
fromEntity.getAvailableBalance().subtract(amount)
);
accountMapper.updateById(fromEntity);
// 增加目标账户余额
toEntity.setAvailableBalance(
toEntity.getAvailableBalance().add(amount)
);
accountMapper.updateById(toEntity);
// ========== 3. 记录Confirm日志 ==========
TransferLog log = new TransferLog();
log.setXid(xid);
log.setActionKey(fromKey);
log.setAction("confirm");
log.setFromAccount(fromAccount);
log.setToAccount(toAccount);
log.setAmount(amount);
log.setStatus("CONFIRMED");
log.setCreateTime(new Date());
transferLogMapper.insert(log);
log.info("Confirm阶段完成:真正扣减{}元 from={}, xid={}", amount, fromAccount, xid);
return true;
} catch (Exception e) {
// Confirm失败会重试,抛出异常
log.error("Confirm阶段失败: xid={}", xid, e);
throw new RuntimeException("Confirm失败", e);
}
}
3.4 业务实现:Cancel阶段
java
@Override
public boolean cancel(BusinessActionContext context) {
String xid = context.getXid();
String fromAccount = (String) context.getActionContext("fromAccount");
BigDecimal amount = new BigDecimal(
context.getActionContext("amount").toString()
);
String fromKey = fromAccount + "_" + xid;
// ========== 1. 检查幂等 ==========
if (transferLogMapper.existsByXidAndAction(fromKey, "cancel")) {
log.info("Cancel阶段已执行过,幂等跳过: xid={}", xid);
return true;
}
// ========== 2. 解冻金额 ==========
Account fromEntity = accountMapper.selectByAccountNo(fromAccount);
if (fromEntity == null) {
// 账户不存在,说明是空回滚
log.warn("Cancel:账户{}不存在,可能是空回滚, xid={}", fromAccount, xid);
// 空回滚也需要记录日志
recordCancelLog(xid, fromKey, fromAccount, amount, "ACCOUNT_NOT_EXISTS");
return true;
}
// ========== 3. 检查冻结金额 ==========
// 如果冻结金额为0或负数,可能是Try没执行(空回滚)或者已经Cancel过了
if (fromEntity.getFrozenBalance().compareTo(amount) < 0) {
// 可能是空回滚:Try没执行或已取消
log.warn("Cancel:冻结金额{} < {}, 可能是空回滚, xid={}",
fromEntity.getFrozenBalance(), amount, xid);
recordCancelLog(xid, fromKey, fromAccount, amount, "INSUFFICIENT_FROZEN");
return true; // 空回滚返回成功
}
// ========== 4. 执行解冻 ==========
BigDecimal newFrozenBalance = fromEntity.getFrozenBalance().subtract(amount);
fromEntity.setFrozenBalance(newFrozenBalance);
// 余额不变,只是释放冻结
accountMapper.updateById(fromEntity);
// ========== 5. 记录Cancel日志 ==========
recordCancelLog(xid, fromKey, fromAccount, amount, "SUCCESS");
log.info("Cancel阶段完成:解冻{}元 from={}, xid={}", amount, fromAccount, xid);
return true;
}
private void recordCancelLog(String xid, String fromKey, String fromAccount,
BigDecimal amount, String reason) {
TransferLog log = new TransferLog();
log.setXid(xid);
log.setActionKey(fromKey);
log.setAction("cancel");
log.setFromAccount(fromAccount);
log.setAmount(amount);
log.setStatus("CANCELLED");
log.setRemark("Cancel原因: " + reason);
log.setCreateTime(new Date());
transferLogMapper.insert(log);
}
四、TCC三剑客:幂等、空回滚、悬挂
4.1 幂等性:保证TCC操作不重复
为什么需要幂等?
Confirm和Cancel可能会执行多次:
- 网络抖动导致超时
- TC(事务协调器)重试
- 服务重启后恢复
所以每个阶段都必须幂等!
java
// 幂等实现方式1:日志表(最常用)
@Service
public class IdempotentService {
@Autowired
private TccActionLogMapper logMapper;
public boolean tryAction(String xid, String action) {
// 查询是否执行过
if (logMapper.exists(xid, action)) {
return true; // 已执行,幂等跳过
}
// 执行业务逻辑
doBusiness();
// 记录执行日志
logMapper.insert(xid, action, "SUCCESS");
return true;
}
}
// 幂等实现方式2:状态机
// 使用状态字段控制
// Try: PENDING -> CONFIRMING
// Cancel: PENDING -> CANCELLING
// CONFIRMING -> CONFIRMED
// CANCELLING -> CANCELLED
4.2 空回滚:Try没执行,Cancel执行了
为什么会出现?
Try超时 → TC认为Try失败 → 触发Cancel
实际上Try可能成功了!
java
@Override
public boolean cancel(BusinessActionContext context) {
String xid = context.getXid();
// 检查Try是否执行过
boolean tryExecuted = tryLogMapper.exists(xid);
if (!tryExecuted) {
// Try没执行,这是空回滚
// 空回滚不需要解冻,直接记录即可
log.warn("空回滚:xid={}", xid);
// 记录空回滚日志
logMapper.insertEmptyCancel(xid, context);
return true;
}
// Try执行过了,正常回滚
return doNormalCancel(context);
}
4.3 悬挂:Cancel比Try先执行
为什么会出现?
Cancel超时 → TC认为Cancel失败,重试Cancel
实际上Cancel可能成功了,Try后执行却因为"已取消"而失败
java
@Override
public boolean tryAction(BusinessActionContext context) {
String xid = context.getXid();
// 检查Cancel是否执行过(防止悬挂)
boolean cancelExecuted = cancelLogMapper.exists(xid, "cancel");
if (cancelExecuted) {
// Cancel已执行,这是悬挂
// Try不能执行,否则会导致数据错误
log.warn("悬挂:Cancel已执行,Try跳过: xid={}", xid);
return true; // 返回成功但不做任何操作
}
// Cancel没执行,正常执行Try
return doNormalTry(context);
}
4.4 三剑客的协同
java
// 完整的三剑客处理
@Override
public boolean tryAction(BusinessActionContext context) {
String xid = context.getXid();
// 1. 幂等检查(防止重复Try)
if (actionLogMapper.exists(xid, "try")) {
return true;
}
// 2. 悬挂检查(Cancel执行过就不能Try)
if (actionLogMapper.exists(xid, "cancel")) {
return true;
}
// 3. 执行Try
doTry();
// 4. 记录日志
actionLogMapper.insert(xid, "try", "SUCCESS");
return true;
}
@Override
public boolean confirm(BusinessActionContext context) {
String xid = context.getXid();
// 1. 幂等检查
if (actionLogMapper.exists(xid, "confirm")) {
return true;
}
// 2. 执行Confirm
doConfirm();
// 3. 记录日志
actionLogMapper.insert(xid, "confirm", "SUCCESS");
return true;
}
@Override
public boolean cancel(BusinessActionContext context) {
String xid = context.getXid();
// 1. 幂等检查
if (actionLogMapper.exists(xid, "cancel")) {
return true;
}
// 2. 空回滚检查(Try没执行)
if (!actionLogMapper.exists(xid, "try")) {
// 空回滚,记录日志即可
actionLogMapper.insert(xid, "cancel", "EMPTY_CANCEL");
return true;
}
// 3. 执行Cancel
doCancel();
// 4. 记录日志
actionLogMapper.insert(xid, "cancel", "SUCCESS");
return true;
}
五、TCC实战:扣款与退款场景
5.1 扣款场景
java
@LocalTCC
public interface DeductionTccService {
@TwoPhaseBusinessAction(
name = "deductAction",
commitMethod = "confirm",
rollbackMethod = "cancel"
)
boolean tryDeduct(
BusinessActionContext context,
@BusinessActionContextParameter(paramName = "accountNo") String accountNo,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount,
@BusinessActionContextParameter(paramName = "orderId") String orderId
);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
@Service
public class DeductionTccServiceImpl implements DeductionTccService {
@Override
public boolean tryDeduct(BusinessActionContext context,
String accountNo, BigDecimal amount, String orderId) {
// 1. 幂等检查
if (deductionLogMapper.existsByOrderId(orderId, "try")) {
return true;
}
// 2. 检查余额
Account account = accountMapper.selectByAccountNo(accountNo);
if (account.getAvailableBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足,无法扣款");
}
// 3. 冻结金额
accountMapper.freeze(accountNo, amount);
// 4. 记录日志
deductionLogMapper.insert(orderId, accountNo, amount, "TRY");
return true;
}
@Override
public boolean confirm(BusinessActionContext context) {
String orderId = (String) context.getActionContext("orderId");
// 幂等检查
if (deductionLogMapper.existsByOrderId(orderId, "confirm")) {
return true;
}
// 真正扣减可用余额
String accountNo = (String) context.getActionContext("accountNo");
BigDecimal amount = new BigDecimal(
context.getActionContext("amount").toString()
);
accountMapper.deductBalance(accountNo, amount);
// 更新日志
deductionLogMapper.updateStatus(orderId, "confirm", "CONFIRMED");
return true;
}
@Override
public boolean cancel(BusinessActionContext context) {
String orderId = (String) context.getActionContext("orderId");
// 幂等检查
if (deductionLogMapper.existsByOrderId(orderId, "cancel")) {
return true;
}
// 空回滚检查
if (!deductionLogMapper.existsByOrderId(orderId, "try")) {
// 空回滚
deductionLogMapper.insert(orderId,
(String) context.getActionContext("accountNo"),
new BigDecimal(context.getActionContext("amount").toString()),
"CANCEL_EMPTY"
);
return true;
}
// 解冻金额
String accountNo = (String) context.getActionContext("accountNo");
BigDecimal amount = new BigDecimal(
context.getActionContext("amount").toString()
);
accountMapper.unfreeze(accountNo, amount);
// 更新日志
deductionLogMapper.updateStatus(orderId, "cancel", "CANCELLED");
return true;
}
}
5.2 退款场景
java
@LocalTCC
public interface RefundTccService {
@TwoPhaseBusinessAction(
name = "refundAction",
commitMethod = "confirm",
rollbackMethod = "cancel"
)
boolean tryRefund(
BusinessActionContext context,
@BusinessActionContextParameter(paramName = "accountNo") String accountNo,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount,
@BusinessActionContextParameter(paramName = "refundId") String refundId
);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
@Service
public class RefundTccServiceImpl implements RefundTccService {
@Override
public boolean tryRefund(BusinessActionContext context,
String accountNo, BigDecimal amount, String refundId) {
// 幂等检查
if (refundLogMapper.existsByRefundId(refundId, "try")) {
return true;
}
// 记录退款日志(待确认)
RefundLog log = new RefundLog();
log.setRefundId(refundId);
log.setAccountNo(accountNo);
log.setAmount(amount);
log.setStatus("PENDING");
log.setCreateTime(new Date());
refundLogMapper.insert(log);
return true;
}
@Override
public boolean confirm(BusinessActionContext context) {
String refundId = (String) context.getActionContext("refundId");
if (refundLogMapper.existsByRefundId(refundId, "confirm")) {
return true;
}
// 增加账户余额
String accountNo = (String) context.getActionContext("accountNo");
BigDecimal amount = new BigDecimal(
context.getActionContext("amount").toString()
);
accountMapper.addBalance(accountNo, amount);
// 更新日志
refundLogMapper.updateStatus(refundId, "CONFIRMED");
return true;
}
@Override
public boolean cancel(BusinessActionContext context) {
String refundId = (String) context.getActionContext("refundId");
if (refundLogMapper.existsByRefundId(refundId, "cancel")) {
return true;
}
// 更新日志(退款取消)
refundLogMapper.updateStatus(refundId, "CANCELLED");
return true;
}
}
六、TCC与Seata集成
6.1 Seata TCC配置
yaml
# Seata TCC模式配置
seata:
enabled: true
application-id: payment-service
tx-service-group: payment-tx-group
# TCC模式配置
tm:
rollback-retry-timeout-unlock-enable: true # 回滚重试超时解锁
rm:
# TCC模式资源自动注册
enable-auto-data-source-proxy: false # TCC不需要自动代理
6.2 资源自动注册
java
// Seata TCC模式下,资源会自动注册
// 但需要配置TCC接口的实现
@LocalTCC
public interface OrderTccService {
@TwoPhaseBusinessAction(
name = "orderAction",
commitMethod = "confirm",
rollbackMethod = "cancel"
)
boolean tryCreateOrder(
BusinessActionContext context,
@BusinessActionContextParameter(paramName = "orderId") String orderId,
@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount
);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
6.3 多服务TCC调用
java
// 跨多个服务的TCC事务
@Service
public class UnifiedPaymentService {
@Autowired
private DeductionTccService deductionTccService;
@Autowired
private RefundTccService refundTccService;
@GlobalTransactional(name = "unified-payment", rollbackFor = Exception.class)
public void unifiedPayment(String userId, BigDecimal amount,
String deductionOrderId, String refundOrderId) {
// 1. 扣款(TCC Try)
deductionTccService.tryDeduct(null, userId, amount, deductionOrderId);
// 2. 退款记录(TCC Try)
refundTccService.tryRefund(null, userId, amount, refundOrderId);
// 如果这里抛出异常:
// - deductionTccService.cancel() 会被调用
// - refundTccService.cancel() 会被调用
// 这就是TCC的自动回滚机制
}
}
七、TCC性能优化
7.1 并行Try阶段
java
// 如果多个TCC调用之间没有依赖,可以并行执行
@Service
public class ParallelTccService {
@GlobalTransactional
public void parallelExecute() {
// 使用CompletableFuture并行执行
CompletableFuture.allOf(
CompletableFuture.runAsync(() -> tccService1.tryAction(...)),
CompletableFuture.runAsync(() -> tccService2.tryAction(...)),
CompletableFuture.runAsync(() -> tccService3.tryAction(...))
).join(); // 等待所有完成
// 任一失败会触发全局回滚
}
}
7.2 异步Confirm
java
// Seata支持异步Confirm,提高性能
@GlobalTransactional(asyncConfirmation = true)
public void asyncConfirmPayment() {
// Try阶段同步执行
deductionTccService.tryDeduct(...);
// Confirm阶段异步执行
// 可以在Seata控制台查看异步任务状态
}
7.3 批量处理
java
// 批量TCC操作,减少网络开销
@LocalTCC
public interface BatchTccService {
@TwoPhaseBusinessAction(name = "batchAction", ...)
boolean tryBatch(BusinessActionContext context,
@BusinessActionContextParameter(paramName = "items") List<BatchItem> items);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
八、TCC监控与问题排查
8.1 TCC事务监控
yaml
# Seata TCC监控指标
management:
metrics:
export:
prometheus:
enabled: true
# 关键指标:
# - seata_tcc_transaction_count: TCC事务总数
# - seata_tcc_transaction_success: TCC事务成功数
# - seata_tcc_transaction_fail: TCC事务失败数
8.2 常见问题排查
bash
# TCC问题排查指南
问题1:Try执行成功,Confirm没执行
原因:Seata Server异常或网络问题
排查:
1. 检查Seata Server状态
2. 查看Seata Server日志
3. 检查网络连通性
问题2:Cancel执行多次
原因:Confirm失败导致重试Cancel
排查:
1. 查看Confirm失败原因
2. 确保Confirm幂等
问题3:空回滚
原因:Try超时或网络问题
排查:
1. 正常现象,说明Try确实没执行
2. 检查Try执行环境和日志
九、踩坑实录
坑1:Try阶段的余额检查时机
某次实现扣款TCC,Try阶段检查余额时扣款100元成功,但Confirm阶段因为数据库问题失败了。
结果:TC触发了Cancel回滚,用户的钱被解冻了。但实际上钱应该被扣掉!
教训:Try阶段只是"告诉我要扣",真正扣钱应该在Confirm。如果Try阶段就真的扣了钱,Confirm失败就会导致问题。
坑2:Cancel时的余额检查
Cancel时检查冻结余额,如果冻结余额不足就跳过解冻。但实际上用户的余额可能已经被其他事务处理了。
教训:Cancel幂等处理,空回滚就记录日志,不要根据余额判断是否执行。
坑3:TCC接口参数不能为null
TCC接口中,BusinessActionContextParameter的参数不能为null,否则序列化会失败。
教训:确保所有@BusinessActionContextParameter标注的参数都有值。
十、总结
TCC是分布式事务的重要模式:
- Try:预留资源,不真正扣减
- Confirm:确认执行,使用预留资源
- Cancel:回滚,释放预留资源
- 幂等:每个阶段必须幂等
- 空回滚:Try没执行时Cancel的处理
- 悬挂:Cancel比Try先执行的防护
最佳实践:
- Try阶段只预留资源,不真正执行
- Confirm/Cancel必须幂等
- 处理空回滚和悬挂
- 使用日志表记录状态
- 合理设置Try超时时间
- 监控TCC事务状态
血的教训:
TCC模式需要业务代码实现补偿逻辑,这意味着业务侵入性较强。如果业务逻辑简单,用AT模式更合适;如果业务复杂、对性能要求高,TCC是更好的选择。
思考题: 你们项目有没有用过TCC模式?Try阶段是真正扣减还是只是冻结?Confirm失败了怎么办?
个人观点,仅供参考