TCC(Try-Confirm-Cancel)是一种应用层的分布式事务解决方案,通过将业务操作拆分为三个阶段来实现最终一致性。
TCC 的核心思想是:把风险控制在 Try 阶段。Try 可以失败、可以回滚;但 Confirm 一旦开始,就必须成功
优缺点:
优点
性能好:不依赖数据库锁,资源预留即可
灵活性高:可以处理复杂业务逻辑
最终一致性:适合对一致性要求不极端严格的场景
可控制:业务方完全控制事务逻辑
缺点
代码侵入性强:需要为每个操作实现三个方法
开发成本高:需要考虑幂等性、悬挂、空回滚等问题
维护复杂:业务逻辑分散在三个方法中
三个阶段
1. Try阶段(该阶段的操作需要保证原子性,各种资源的预留要准确)
只做检查和资源冻结,目的是在操作多个数据源之前做检查和校对,保证Confirm的成功概率。因为进入Confirm后,多个数据源之间是没有事务的关联的。
2. Confirm阶段(一旦开始,就必须最终全部成功;不能半途而废,也不能回滚),要求幂等
真实触发和处理业务,这里执行真实业务,因为有前面的try检查,这里的成功率会很高。
3. Cancel阶段(处理了空回滚后,直接根据事务唯一id,进行资源的释放),要求幂等
Try阶段任一失败后执行
取消所有已执行的Try操作,释放预留的资源
异常处理(因为有3个阶段,其try阶段,重复触发可以当作重复下订单,无所谓,但是Confirm和Cancel不可重复触发或者异常触发)
空悬挂(空回滚):因为网络原因,tryTransfer函数5秒后才返回,而协调者设置了3秒超时就调用cancelTransfer函数,则会造成空回滚;
重复确认:因为网络原因,confirmTransfer被重复触发;
// 定义一个TCC业务对象
public class TransferOB(){
String fromAccount;// 付款账号
String toAccount;// 收款账号
BigDecimal amount;// 金额
String txId;// 事务唯一id,可以使用uuid
}
// TCC接口定义
public interface TransferTccService {
/**
* Try阶段:预留资源
*/
boolean tryTransfer(TransferOB t);
/**
* Confirm阶段:确认执行
*/
boolean confirmTransfer(TransferOB t);
/**
* Cancel阶段:取消执行,释放资源
*/
boolean cancelTransfer(TransferOB t);
}
@Service
public class TransferTccServiceImpl implements TransferTccService {
@Autowired
private AccountDao accountDao;
@Autowired
private FreezeRecordDao freezeRecordDao;
@Override
@Transactional
public boolean tryTransfer(TransferOB t) {
String fromAccount = t.getFromAccount();
String toAccount = t.getToAccount();
BigDecimal amount = t.getAmount();
String txId = t.getTxId();
// 检查是否已回滚(防悬挂)
if (freezeRecordDao.isRolledBack(txId)) {
throw new RuntimeException("Transaction already rolled back");
}
// 检查余额
BigDecimal balance = accountDao.getBalance(fromAccount);// 余额
BigDecimal freeze = freezeRecordDao.getBalance(fromAccount);// 已冻结金额
BigDecimal nowBalance = balance.subtract(freeze);
if (nowBalance.compareTo(amount) < 0) {
return false;
}
// 新增冻结金额(预留资源)
freezeRecordDao.insert(t);
return true;
}
@Override
@Transactional
public boolean confirmTransfer(TransferOB t) {
String fromAccount = t.getFromAccount();
String toAccount = t.getToAccount();
BigDecimal amount = t.getAmount();
String txId = t.getTxId();
// 幂等检查
if (freezeRecordDao.isConfirmed(txId)) {
return true;// 已经处理过了,无需重复转账(防止重试造成异常)
}
// 扣款
// -- 扣总余额(可以利用数据库自身的行级锁,保证不透支)
// UPDATE account
// SET total_balance = total_balance - #{amount}
// WHERE account_id = #{accountId} AND total_balance >= #{amount};
accountDao.debit(fromAccount, amount);
// 加款
accountDao.credit(toAccount, amount);
// 删除冻结记录(即释放资源,如果还有别的资源也要一起释放)
freezeRecordDao.delete(txId);
// 标记为已确认
transferRecordService.markAsConfirmed(txId);
return true;
}
@Override
@Transactional
public boolean cancelTransfer(TransferOB t) {
String fromAccount = t.getFromAccount();
String toAccount = t.getToAccount();
BigDecimal amount = t.getAmount();
String txId = t.getTxId();
// 空回滚检查
if (!freezeRecordDao.exists(txId)) {
freezeRecordDao.insertEmptyRollback(txId);// 插入空回滚记录,在tryTransfer中检查
return true;
}
// 删除冻结记录(即释放资源,如果还有别的资源也要一起释放)
freezeRecordDao.delete(txId);
return true;
}
}