借助Nacos高效配置與實踐Seata事務的TCC模式
實現
TCC 模式
TCC模式與AT模式非常相似,每階段都是獨立事務,不同的是TCC通過人工編碼來實現數據恢復。需要實現三個方法:
- Try:資源的檢測和預留;
- Confirm:完成資源操作業(yè)務;要求 Try 成功 Confirm 一定要能成功。
- Cancel:預留資源釋放,可以理解為try的反向操作。
流程分析
圖片
階段一(Try):檢查余額是否充足,如果充足則凍結金額增加30元,可用余額扣除30
圖片
圖片
此時,總金額 = 凍結金額 + 可用金額,數量依然是100不變,事務直接提交無需等待其它事務。
階段二(Confirm) :假如要提交,則凍結金額扣減30
圖片
確認可以提交,不過之前可用金額已經扣減過了,這里只要清除凍結金額就好了,此時,總金額 = 凍結金額 + 可用金額 = 0 + 70 = 70
階段二(Cancel):如果要回滾,則凍結金額扣減30,可用余額增加30
圖片
需要回滾,那么就要釋放凍結金額,恢復可用金額
Seata的TCC模型
圖片
代碼樣例
配置和依賴參考之前《利用Nacos實現Seata事務模式(XA與AT)的快速配置與靈活切換》即可
bank3:
聲明TCC接口
@LocalTCC
public interface AccountInTcc {
@TwoPhaseBusinessAction(name = "prepareDeductMoney", commitMethod = "commitDeductMoney", rollbackMethod = "rollbackDeductMoney")
boolean prepareDeductMoney(BusinessActionContext businessActionContext,
@BusinessActionContextParameter(paramName = "accountNo")String accountNo,
@BusinessActionContextParameter(paramName = "amount")Double amount);
/**
* 提交扣款
* 二階段confirm確認方法、可以另命名,但要保證與commitMethod一致
*/
boolean commitDeductMoney(BusinessActionContext businessActionContext);
/**
* 回滾扣款
* 二階段回滾方法,要保證與rollbackMethod一致
*/
boolean rollbackDeductMoney(BusinessActionContext businessActionContext);
}
具體實現:
@Component
public class AccountInTccImpl implements AccountInTcc {
@Autowired
private AccountInfoMapper accountInfoMapper;
@Transactional
@Override
public boolean prepareDeductMoney(BusinessActionContext businessActionContext, String accountNo, Double amount) {
String xid = businessActionContext.getXid();
// 冪等性判斷
if (TccActionResultWrap.hasPrepareResult(xid)) {
return true;
}
// 避免空懸掛,已經執(zhí)行過回滾了就不能再預留資源
if (TccActionResultWrap.hasRollbackResult(xid) || TccActionResultWrap.hasCommitResult(xid)) {
return false;
}
// 預留資源
boolean result = accountInfoMapper.prepareDeductMoney(accountNo,amount) > 0;
// 記錄執(zhí)行結果,以便回滾時判斷是否是空回滾
TccActionResultWrap.prepareSuccess(xid);
System.out.println("============prepare==============");
return result;
}
// 保證提交邏輯的原子性
@Transactional
@Override
public boolean commitDeductMoney(BusinessActionContext businessActionContext) {
String xid = businessActionContext.getXid();
// 冪等性判斷
if (TccActionResultWrap.hasCommitResult(xid)) {
return true;
}
Map<String, Object> actionContext = businessActionContext.getActionContext();
String accountNo = (String) actionContext.get("accountNo");
BigDecimal amount = (BigDecimal) actionContext.get("amount");
// 執(zhí)行提交操作,扣除預留款
boolean result = accountInfoMapper.commitDeductMoney(accountNo,amount.doubleValue()) > 0;
// 清除預留結果
TccActionResultWrap.removePrepareResult(xid);
// 設置提交結果
TccActionResultWrap.commitSuccess(xid);
System.out.println("============commit==============");
return result;
}
@Transactional
@Override
public boolean rollbackDeductMoney(BusinessActionContext businessActionContext) {
String xid = businessActionContext.getXid();
// 冪等性判斷
if (TccActionResultWrap.hasRollbackResult(xid)) {
return true;
}
// 沒有預留資源結果,回滾不做任何處理;
if (!TccActionResultWrap.hasPrepareResult(xid)) {
// 設置回滾結果,防止空回滾
TccActionResultWrap.rollbackSuccess(xid);
return true;
}
// 執(zhí)行回滾
Map<String, Object> actionContext = businessActionContext.getActionContext();
String accountNo = (String) actionContext.get("accountNo");
BigDecimal amount = (BigDecimal) actionContext.get("amount");
boolean result = accountInfoMapper.rollbackDeductMoney(accountNo,amount.doubleValue()) > 0;
// 清除預留結果
TccActionResultWrap.removePrepareResult(xid);
// 設置回滾結果
TccActionResultWrap.rollbackSuccess(xid);
System.out.println("============rollback==============");
return result;
}
}
業(yè)務層:
@Autowired
private AccountInTcc accountInTcc;
@Override
public Boolean deductMoney(String accountNo, Double amount) {
return accountInTcc.prepareDeductMoney(null,accountNo,amount);
}
參數中的BusinessActionContext不需要開發(fā)人員自己傳遞,直接給null即可,Seata會自動處理。
mapper:
@Update("update account_info set account_balance = account_balance - #{amount}, frozen_money = frozen_money + #{amount} where account_no = #{accountNo} and account_balance >= #{amount}")
int prepareDeductMoney(@Param("accountNo") String accountNo, @Param("amount") Double amount);
@Update("update account_info set frozen_money = frozen_money - #{amount} where account_no = #{accountNo}")
int commitDeductMoney(@Param("accountNo") String accountNo, @Param("amount") Double amount);
@Update("update account_info set account_balance = account_balance + #{amount}, frozen_money = frozen_money - #{amount} where account_no = #{accountNo}")
int rollbackDeductMoney(@Param("accountNo") String accountNo, @Param("amount") Double amount);
bank4服務調用:
@GlobalTransactional
@Override
public Boolean addMoney(String accountNo, Double amount) {
String result = bank3Client.deduct(amount);
if("true".equalsIgnoreCase(result)){
Boolean flag = baseMapper.addMoney(accountNo,amount) > 0;
if(amount != 30 ) throw new RuntimeException("bank4 make exception amount != 30");
return flag;
}
return false;
}
TCC的優(yōu)點:
- 一階段完成直接提交事務,釋放數據庫資源,性能好
- 相比AT模型,無需生成快照,無需使用全局鎖,性能最強
- 不依賴數據庫事務,而是依賴補償操作,可以用于非事務型數據庫
TCC的缺點:
- 有代碼侵入,需要人為編寫try、Confirm和Cancel接口,太麻煩
- 軟狀態(tài),事務是最終一致
- 需要考慮Confirm和Cancel的失敗情況,做好冪等處理
- 空回滾:當某分支事務的try階段阻塞時,可能導致全局事務超時而觸發(fā)二階段的cancel操作。在未執(zhí)行try操作時先執(zhí)行了cancel操作,這時cancel不能做回滾,就是空回滾
- 業(yè)務懸掛:對于已經空回滾的業(yè)務,之前被阻塞的try操作恢復,繼續(xù)執(zhí)行try,就永遠不可能confirm或cancel ,事務一直處于中間狀態(tài),這就是業(yè)務懸掛。
圖片
執(zhí)行cancel操作時,應當判斷try是否已經執(zhí)行,如果尚未執(zhí)行,則應該空回滾。
執(zhí)行try操作時,應當判斷cancel是否已經執(zhí)行過了,如果已經執(zhí)行,應當阻止空回滾后的try操作,避免懸掛。