新聞中心
1. 什么是 TCC 模式
相比于上篇文章所聊的 AT 模式,TCC(Try-Confirm-Cancel) 模式就帶一點手動的感覺了,它也是兩階段提交的演化,但是和 AT 又不太一樣,我們來看下流程。

專注于為中小企業(yè)提供做網(wǎng)站、成都網(wǎng)站設(shè)計服務(wù),電腦端+手機端+微信端的三站合一,更高效的管理,為中小企業(yè)和布克賽爾蒙古免費做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動了千余家企業(yè)的穩(wěn)健成長,幫助中小企業(yè)通過網(wǎng)站建設(shè)實現(xiàn)規(guī)模擴充和轉(zhuǎn)變。
官網(wǎng)上有一張 TCC 的流程圖,我們來看下:
可以看到,TCC 也是分為兩階段:
第一階段是 prepare,在這個階段主要是做資源的檢測和預(yù)留工作,例如銀行轉(zhuǎn)賬,這個階段就先去檢查下用戶的錢夠不夠,不夠就直接拋異常,夠就先給凍結(jié)上。
第二階段是 commit 或 rollback,這個主要是等各個分支事務(wù)的一階段都執(zhí)行完畢,都執(zhí)行完畢后各自將自己的情況報告給 TC,TC 一統(tǒng)計,發(fā)現(xiàn)各個分支事務(wù)都沒有異常,那么就通知大家一起提交;如果 TC 發(fā)現(xiàn)有分支事務(wù)發(fā)生異常了,那么就通知大家回滾。
那么小伙伴可能也發(fā)現(xiàn)了,上面這個流程中,一共涉及到了三個方法,prepare、commit 以及 rollback,這三個方法都完全是用戶自定義的方法,都是需要我們自己來實現(xiàn)的,所以我一開始就說 TCC 是一種手動的模式。
和 AT 相比,大家發(fā)現(xiàn) TCC 這種模式其實是不依賴于底層數(shù)據(jù)庫的事務(wù)支持的,也就是說,哪怕你底層數(shù)據(jù)庫不支持事務(wù)也沒關(guān)系,反正 prepare、commit 以及 rollback 三個方法都是開發(fā)者自己寫的,我們自己將這三個方法對應(yīng)的流程捋順就行了。
在上篇文章的中,我們講 AT 模式,每個數(shù)據(jù)庫都需要有一個 undo log 表,這個表用來記錄一條數(shù)據(jù)更改之前和更改之后的狀態(tài)(前鏡像和后鏡像),如果所有分支事務(wù)最終都提交成功,那么記錄在 undo log 表中的數(shù)據(jù)就會自動刪除;如果有一個分支事務(wù)執(zhí)行失敗,導(dǎo)致所有事務(wù)都需要回滾,那么就會以 undo log 表中的數(shù)據(jù)會依據(jù),生成反向補償語句,利用反向補償語句將數(shù)據(jù)復(fù)原,執(zhí)行完成后也會刪除 undo log 表中的記錄。
在這個流程中,大家看到,undo log 表扮演了非常重要的角色。TCC 和 AT 最大的區(qū)別在于,TCC 中的提交和回滾邏輯都是開發(fā)者自己寫的,而 AT 都是框架自動完成的。
為了方便大家理解,本文我就不重新搞案例了,咱們還用上篇文章那個下訂單的案例來演示。
2. 案例回顧
這是一個商品下單的案例,一共有五個服務(wù),我來和大家稍微解釋下:
- eureka:這是服務(wù)注冊中心。
- account:這是賬戶服務(wù),可以查詢/修改用戶的賬戶信息(主要是賬戶余額)。
- order:這是訂單服務(wù),可以下訂單。
- storage:這是一個倉儲服務(wù),可以查詢/修改商品的庫存數(shù)量。
- bussiness:這是業(yè)務(wù),用戶下單操作將在這里完成。
這個案例講了一個什么事呢?
當(dāng)用戶想要下單的時候,調(diào)用了 bussiness 中的接口,bussiness 中的接口又調(diào)用了它自己的 service,在 service 中,首先開啟了全局分布式事務(wù),然后通過 feign 調(diào)用 storage 中的接口去扣庫存,然后再通過 feign 調(diào)用 order 中的接口去創(chuàng)建訂單(order 在創(chuàng)建訂單的時候,不僅會創(chuàng)建訂單,還會扣除用戶賬戶的余額),在這個過程中,如果有任何一個環(huán)節(jié)出錯了(余額不足、庫存不足等導(dǎo)致的問題),就會觸發(fā)整體的事務(wù)回滾。
本案例具體架構(gòu)如下圖:
這個案例就是一個典型的分布式事務(wù)問題,storage、order 以及 account 中的事務(wù)分屬于不同的微服務(wù),但是我們希望他們同時成功或者同時失敗。
這個案例的基本架構(gòu)我這里就不重復(fù)搭建了,小伙伴們可以參考上篇文章,這里我們主要來看 TCC 事務(wù)如何添加進(jìn)來。
3. 重新設(shè)計數(shù)據(jù)庫
首先我們將??上篇文章??中的數(shù)據(jù)庫來重新設(shè)計一下,方便我們本文的使用。
賬戶表增加一個凍結(jié)金額的字段,如下:
訂單表和前文保持一致,不變。
庫存表也增加一個凍結(jié)庫存數(shù)量的字段,如下:
另外,由于我們這里不再使用 AT 模式,所以可以刪除之前的 undo_log 表了(可能有小伙伴刪除 undo_log 表之后,會報錯,那是因為你 TCC 模式使用不對,注意看松哥后面的講解哦)。
相關(guān)的數(shù)據(jù)庫腳本小伙伴們可以在文末下載,這里我就不列出來了。
4. 重新設(shè)計 Feign 接口
在 TCC 模式中,我們的 Feign 換一種方式來配置。
小伙伴們都知道,在??上篇文章??的案例中,我們有一個 common 模塊,用來存放一些公共內(nèi)容(實際上我們只是存儲了 RespBean),現(xiàn)在我們把這里涉及到的 OpenFeign 接口也存儲進(jìn)來,一共是三個 OpenFeign 接口,因為還要用到 seata 中的注解,所以我們在 common 中引入 OpenFeign 和 seata 的依賴,如下:
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
2.2.2.RELEASE
org.springframework.cloud
spring-cloud-starter-openfeign
2.2.6.RELEASE
然后在這里定義 OpenFeign 的三個接口,如下:
@LocalTCC
public interface AccountServiceApi {
@PostMapping("/account/deduct/prepare")
@TwoPhaseBusinessAction(name = "accountServiceApi", commitMethod = "commit", rollbackMethod = "rollback")
boolean prepare(@RequestBody BusinessActionContext actionContext, @RequestParam("userId") @BusinessActionContextParameter(paramName = "userId") String userId, @RequestParam("money") @BusinessActionContextParameter(paramName = "money") Double money);
@RequestMapping("/account/deduct/commit")
boolean commit(@RequestBody BusinessActionContext actionContext);
@RequestMapping("/account/deduct/rollback")
boolean rollback(@RequestBody BusinessActionContext actionContext);
}
@LocalTCC
public interface OrderServiceApi {
@PostMapping("/order/create/prepare")
@TwoPhaseBusinessAction(name = "orderServiceApi", commitMethod = "commit", rollbackMethod = "rollback")
boolean prepare(@RequestBody BusinessActionContext actionContext, @RequestParam("userId") @BusinessActionContextParameter(paramName = "userId") String userId, @RequestParam("productId") @BusinessActionContextParameter(paramName = "productId") String productId, @RequestParam("count") @BusinessActionContextParameter(paramName = "count") Integer count);
@RequestMapping("/order/create/commit")
boolean commit(@RequestBody BusinessActionContext actionContext);
@RequestMapping("/order/create/rollback")
boolean rollback(@RequestBody BusinessActionContext actionContext);
}
@LocalTCC
public interface StorageServiceApi {
@PostMapping("/storage/deduct/prepare")
@TwoPhaseBusinessAction(name = "storageServiceApi",commitMethod = "commit",rollbackMethod = "rollback")
boolean deduct(@RequestBody BusinessActionContext actionContext, @RequestParam("productId")@BusinessActionContextParameter(paramName = "productId") String productId, @RequestParam("count") @BusinessActionContextParameter(paramName = "count") Integer count);
@RequestMapping("/storage/deduct/commit")
boolean commit(@RequestBody BusinessActionContext actionContext);
@RequestMapping("/storage/deduct/rollback")
boolean rollback(@RequestBody BusinessActionContext actionContext);
}
這里一共有三個接口,但是只要大家搞懂其中一個,另外兩個都很好懂了。我這里就以 AccountServiceApi 為例來和大家講解吧。
- 首先接口的定義上,需要加一個注解 @LocalTCC,這個表示開啟 seata 中的 TCC 模式。
- 然后就是 @TwoPhaseBusinessAction 注解,兩階段提交的注解,這個注解有三個屬性,第一個 name 就是處理兩階段提交的 bean 的名字,其實就是當(dāng)前 bean 的名字,當(dāng)前類名首字母小寫。兩階段第一階段就是 prepare 階段,也就是執(zhí)行 @TwoPhaseBusinessAction 注解所在的方法,第二階段則分為兩種情況,提交或者回滾,分別對應(yīng)了兩個不同的方法,commitMethod 和 rollbackMethod 就指明了相應(yīng)的方法。
- 一階段的 prepare 需要開發(fā)者手動調(diào)用,二階段的 commit 或者 rollback 則是系統(tǒng)自動調(diào)用。prepare 中的方法是由開發(fā)者來傳遞的,而在二階段的方法中,相關(guān)的參數(shù)我們需要從 BusinessActionContext 中獲取,@BusinessActionContextParameter 注解就是將對應(yīng)的參數(shù)放入到 BusinessActionContext 中(注意需要給每一個參數(shù)取一個名字),將來可以從 BusinessActionContext 中取出對應(yīng)的參數(shù)。
- 另外需要注意,接口的返回值設(shè)計成 boolean,用以表示相應(yīng)的操作執(zhí)行成功還是失敗,返回 false 表示執(zhí)行失敗,默認(rèn)會有重試機制進(jìn)行重試。
這是 AccountServiceApi,另外兩個接口的設(shè)計也是大同小異,這里我就不再贅述。
接下來看接口的實現(xiàn)。
5. Account
首先我們來看看 Account 服務(wù)。AccountController 實現(xiàn) AccountServiceApi。
我們來看下 AccountController 的定義:
@RestController
public class AccountController implements AccountServiceApi {
@Autowired
AccountService accountService;
@Override
public boolean prepare(BusinessActionContext actionContext, String userId, Double money) {
return accountService.prepareDeduct(userId, money);
}
@Override
public boolean commit(BusinessActionContext actionContext) {
return accountService.commitDeduct(actionContext);
}
@Override
public boolean rollback(BusinessActionContext actionContext) {
return accountService.rollbackDeduct(actionContext);
}
}
因為接口的路徑都定義在 AccountServiceApi 中了,所以這里只需要簡單實現(xiàn)即可,核心的處理邏輯在 AccountService 中,我們來看下 AccountService:
@Service
public class AccountService {
private static final Logger logger = LoggerFactory.getLogger(AccountService.class);
@Autowired
AccountMapper accountMapper;
/**
* 預(yù)扣款階段
* 檢查賬戶余額
*
* @param userId
* @param money
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean prepareDeduct(String userId, Double money) {
Account account = accountMapper.getAccountByUserId(userId);
if (account == null) {
throw new RuntimeException("賬戶不存在");
}
if (account.getMoney() < money) {
throw new RuntimeException("余額不足,預(yù)扣款失敗");
}
account.setFreezeMoney(account.getFreezeMoney() + money);
account.setMoney(account.getMoney() - money);
Integer i = accountMapper.updateAccount(account);
logger.info("{} 賬戶預(yù)扣款 {} 元", userId, money);
return i == 1;
}
/**
* 實際扣款階段
*
* @param actionContext
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean commitDeduct(BusinessActionContext actionContext) {
String userId = (String) actionContext.getActionContext("userId");
Double money = ((BigDecimal) actionContext.getActionContext("money")).doubleValue();
Account account = accountMapper.getAccountByUserId(userId);
if (account.getFreezeMoney() < money) {
throw new RuntimeException("余額不足,扣款失敗");
}
account.setFreezeMoney(account.getFreezeMoney() - money);
Integer i = accountMapper.updateAccount(account);
logger.info("{} 賬戶扣款 {} 元", userId, money);
return i == 1;
}
/**
* 賬戶回滾階段
*
* @param actionContext
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean rollbackDeduct(BusinessActionContext actionContext) {
String userId = (String) actionContext.getActionContext("userId");
Double money = ((BigDecimal) actionContext.getActionContext("money")).doubleValue();
Account account = accountMapper.getAccountByUserId(userId);
if (account.getFreezeMoney() >= money) {
account.setMoney(account.getMoney() + money);
account.setFreezeMoney(account.getFreezeMoney() - money);
Integer i = accountMapper.updateAccount(account);
logger.info("{} 賬戶釋放凍結(jié)金額 {} 元", userId, money);
return i == 1;
}
logger.info("{} 賬戶資金已釋放",userId);
//說明prepare中拋出異常,未凍結(jié)資金
return true;
}
}
- AccountService 里一共有三個方法,在整個兩階段提交中,一階段執(zhí)行 prepareDeduct 方法,二階段執(zhí)行 commitDeduct 或者 rollbackDeduct 方法。
- 在 prepareDeduct 中,我們主要檢查一下賬戶是否存在,賬戶余額是否充足,余額充足就將本次消費的金額凍結(jié)起來,凍結(jié)的邏輯就是給 freezeMoney 字段增加本次消費金額,從 money 字段減少本次消費金額。
- 等到其他幾個服務(wù)的一階段方法都執(zhí)行完成后,都沒有拋出異常,此時就執(zhí)行二階段的提交方法,對應(yīng)這里就是 commitDeduct 方法;如果其他服務(wù)的一階段執(zhí)行過程中,拋出了異常,那么就執(zhí)行二階段的回滾方法,對應(yīng)這里的 rollbackDeduct。
- 在 commitDeduct 方法中,首先從 BusinessActionContext 中提取出來我們需要的參數(shù)(因為這個方法是系統(tǒng)自動調(diào)用的,不是我們手動調(diào)用,因此沒法自己傳參數(shù)進(jìn)來,只能通過 BusinessActionContext 來獲?。缓笤贆z查一下余額是否充足,沒問題就把凍結(jié)的資金劃掉,就算扣款完成了。
- 在 rollbackDeduct 方法中,也是先從 BusinessActionContext 中獲取相應(yīng)的參數(shù),檢查一下凍結(jié)的金額,沒問題就把凍結(jié)的金額恢復(fù)到 money 字段上(如果沒進(jìn)入 if 分支,則說明 prepare 中拋出異常,未凍結(jié)資金)。
好了,這就是從賬戶扣錢的兩階段操作,數(shù)據(jù)庫操作比較簡單,我這里就不列出來了,文末可以下載源碼。
6. Order
再來看訂單服務(wù)。
由于我們是在 order 中調(diào)用 account 完成賬戶扣款的,所以需要先在 order 中加入 account 的 OpenFeign 調(diào)用,如下:
@FeignClient("account")
public interface AccountServiceApiImpl extends AccountServiceApi {
}這應(yīng)該沒啥好解釋的。
接下來我們來看 OrderController:
@RestController
public class OrderController implements OrderServiceApi {
@Autowired
OrderService orderService;
@Override
public boolean prepare(BusinessActionContext actionContext, String userId, String productId, Integer count) {
return orderService.prepareCreateOrder(actionContext,userId, productId, count);
}
@Override
public boolean commit(BusinessActionContext actionContext) {
return orderService.commitOrder(actionContext);
}
@Override
public boolean rollback(BusinessActionContext actionContext) {
return orderService.rollbackOrder(actionContext);
}
}
這個跟 AccountService 也基本一致,實現(xiàn)了 OrderServiceApi 接口,接口地址啥的都定義在 OrderServiceApi 中,這個類重點還是在 OrderService 中,如下:
@Service
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
@Autowired
AccountServiceApi accountServiceApi;
@Autowired
OrderMapper orderMapper;
@Transactional(rollbackFor = Exception.class)
public boolean prepareCreateOrder(BusinessActionContext actionContext, String userId, String productId, Integer count) {
//先去扣款,假設(shè)每個產(chǎn)品100塊錢
boolean resp = accountServiceApi.prepare(actionContext, userId, count * 100.0);
logger.info("{} 用戶購買的 {} 商品共計 {} 件,預(yù)下單成功", userId, productId, count);
return resp;
}
@Transactional(rollbackFor = Exception.class)
public boolean commitOrder(BusinessActionContext actionContext) {
String userId = (String) actionContext.getActionContext("userId");
String productId = (String) actionContext.getActionContext("productId");
Integer count = (Integer) actionContext.getActionContext("count");
int i = orderMapper.addOrder(userId, productId, count, count * 100.0);
logger.info("{} 用戶購買的 {} 商品共計 {} 件,下單成功", userId, productId, count);
return i==1;
}
@Transactional(rollbackFor = Exception.class)
public boolean rollbackOrder(BusinessActionContext actionContext) {
String userId = (String) actionContext.getActionContext("userId");
String productId = (String) actionContext.getActionContext("productId");
Integer count = (Integer) actionContext.getActionContext("count");
logger.info("{} 用戶購買的 {} 商品共計 {} 件,訂單回滾成功", userId, productId, count);
return true;
}
}
跟之前的 AccountService 一樣,這里也是三個核心方法:
- prepareCreateOrder:這里主要是調(diào)用了一下賬戶的方法,去檢查下看下錢夠不。一階段就做個這事。
- commitOrder:二階段如果是提交的話,就向數(shù)據(jù)庫中添加一條訂單記錄。
- rollbackOrder:二階段如果是回滾的話,就什么事情都不做,打個日志就行了。
好了,這就是下單的操作。
7. Storage
最后我們再來看看扣庫存的操作,這個跟扣款比較像,一起來看下:
@RestController
public class StorageController implements StorageServiceApi {
@Autowired
StorageService storageService;
@Override
public boolean deduct(BusinessActionContext actionContext, String productId, Integer count) {
return storageService.prepareDeduct(productId, count);
}
@Override
public boolean commit(BusinessActionContext actionContext) {
return storageService.commitDeduct(actionContext);
}
@Override
public boolean rollback(BusinessActionContext actionContext) {
return storageService.rollbackDeduct(actionContext);
}
}
核心邏輯在 StorageService 中,如下:
@Service
public class StorageService {
private static final Logger logger = LoggerFactory.getLogger(StorageService.class);
@Autowired
StorageMapper storageMapper;
/**
* 預(yù)扣庫存
*
* @param productId
* @param count
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean prepareDeduct(String productId, Integer count) {
Storage storage = storageMapper.getStorageByProductId(productId);
if (storage == null) {
throw new RuntimeException("商品不存在");
}
if (storage.getCount() < count) {
throw new RuntimeException("庫存不足,預(yù)扣庫存失敗");
}
storage.setFreezeCount(storage.getFreezeCount() + count);
storage.setCount(storage.getCount() - count);
int i = storageMapper.updateStorage(storage);
logger.info("{} 商品庫存凍結(jié) {} 個", productId, count);
return i == 1;
}
/**
* 扣庫存
*
* @param actionContext
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean commitDeduct(BusinessActionContext actionContext) {
String productId = (String) actionContext.getActionContext("productId");
Integer count = (Integer) actionContext.getActionContext("count");
Storage storage = storageMapper.getStorageByProductId(productId);
if (storage.getFreezeCount() < count) {
throw new RuntimeException("庫存不足,扣庫存失敗");
}
storage.setFreezeCount(storage.getFreezeCount() - count);
int i = storageMapper.updateStorage(storage);
logger.info("{} 商品庫存扣除 {} 個", productId, count);
return i == 1;
}
@Transactional(rollbackFor = Exception.class)
public boolean rollbackDeduct(BusinessActionContext actionContext) {
String productId = (String) actionContext.getActionContext("productId");
Integer count <
新聞名稱:聽說TCC不支持OpenFeign?這個坑松哥必須給大家填了!
文章地址:http://m.fisionsoft.com.cn/article/dpdopjg.html


咨詢
建站咨詢
