本文先經過分佈式事務中tcc方案,衍生出seata的tcc模式,主要仍是會經過代碼示例來作介紹。github代碼地址可提早下載,該項目中包括數據庫、seata配置,以及全部分佈式服務的所有代碼。你們若是想練練手,能夠先拉取該項目代碼,再結合本文學習。核心配置環境以下:java
環境類型 | 版本號 |
---|---|
jdk | 1.8.0_251 |
mysql | 8.0.22 |
seata server | 1.4.1 |
咱們前面有幾篇文章都有介紹過度布式事務的方案,目前常見的分佈式事務方案有:2pc、tcc和異步確保型。以前講過用jta atomikos實現多數據源的 2pc
,用 異步確保型
方案實現支付業務的事務等等,就是沒專門講過 tcc
的應用。node
由於tcc方案的操做難度仍是比較大的。不能單打獨鬥,最好須要依託一個成熟的框架來實現。常見的tcc開源框架有tcc-transaction、Hmily和ByteTCC等,不過他們不像seata背靠大廠,沒法提供持續的維護,所以我更推薦seata的tcc方案。mysql
先說說seata吧,分佈式事務的解決方案確定不侷限於上面說的三種,實際上五花八門。由於它的確很讓人頭疼,各位大神都想研發出最好用的框架。本文的主角 - seata
,就是阿里的一個開源項目。git
seata提供了AT、TCC、SAGA 和 XA,一共4種事務模式。像AT模式就很受歡迎,咱們在實現多數據源的事務一致性時,一般會選用 2PC
的方案,等待全部數據源的事務執行成功,最後再一塊兒提交事務。這個等待全部數據源事務執行的過程就比較耗時,即影響性能,也不安全。github
而seata AT模式的作法就很靈活,它學習數據庫的 undo log,每一個事務執行時當即提交事務,但會把 undo 的回退sql記錄下來。若是全部事務執行成功,清除記錄 undo sql的行記錄,若是某個事務失敗,則執行對應 undo sql 回滾數據。在保證事務的同時,併發量也大了起來。redis
但咱們今天要講的是 seata TCC 模式,若是你對 Seata的其餘模式感興趣,能夠上官網瞭解。spring
先講一下示例的業務吧,咱們仍是拿比較經典的電商支付場景舉例。假設支付成功後,涉及到三個系統事務:sql
按照tcc(try-confirm-cancel)的思路,這三個事務能夠分別分解成下面的過程。數據庫
訂單系統 order
庫存系統 storage
帳戶系統 account
爲了模擬分佈式事務,上述的不一樣系統業務,咱們經過在不一樣數據庫中建立表結構來模擬。固然tcc的分佈式事務不侷限於數據庫層面,還包括http接口調用和rpc調用等,可是殊途同歸,能夠做爲示例參考。json
下面先列出三張業務表的表結構,具體的sql可見最後附件。
表:order
列名 | 類型 | 備註 |
---|---|---|
id | int | 主鍵 |
order_no | varchar | 訂單號 |
user_id | int | 用戶id |
product_id | int | 產品id |
amount | int | 數量 |
money | decimal | 金額 |
status | int | 訂單狀態:0:建立中;1:已完結 |
表:storage
列名 | 類型 | 備註 |
---|---|---|
id | int | 主鍵 |
product_id | int | 產品id |
residue | int | 剩餘庫存 |
frozen | int | TCC事務鎖定的庫存 |
表:account
列名 | 類型 | 備註 |
---|---|---|
id | int | 主鍵 |
user_id | int | 用戶id |
residue | int | 剩餘可用額度 |
frozen | int | TCC事務鎖定的金額 |
seata server 的安裝包可直接從官方github下載,下載壓縮包後,解壓到本地或服務器上。
Seata Server 的配置文件有兩個:
registry.conf
Seata Server 要向註冊中心進行註冊,這樣,其餘服務就能夠經過註冊中心去發現 Seata Server,與 Seata Server 進行通訊。Seata 支持多款註冊中心服務:nacos 、eureka、redis、zk、consul、etcd三、sofa。咱們項目中要使用 eureka 註冊中心,eureka服務的鏈接地址、註冊的服務名,這須要在 registry.conf 文件中對 registry
進行配置。
Seata 須要存儲全局事務信息、分支事務信息、全局鎖信息,這些數據存儲到什麼位置?針對存儲位置的配置,支持放在配置中心,或者也能夠放在本地文件。Seata Server 支持的配置中心服務有:nacos 、apollo、zk、consul、etcd3。這裏咱們選擇最簡單的,使用本地文件,這須要在 registry.conf 文件中對 config
進行配置。
file.conf
file.conf 中對事務信息的存儲位置進行配置,存儲位置支持:file、db、redis。
這裏咱們選擇數據庫做爲存儲位置,這須要在 file.conf 中進行配置。
執行 seata/bin/seata-server.sh(windows 是 seata-server.bat) 腳本便可啓動seata server。還能夠配置下列參數:
-h:註冊到註冊中心的ip -p:server rpc 監聽端口,默認 8091 -m:全局事務會話信息存儲模式,file、db,優先讀取啓動參數 -n:server node,多個server時,須要區分各自節點,用於生成不一樣區間的transctionId,以避免衝突 -e:多環境配置
mysql 8
默認啓動後會報mysql-connector-java-x.jar
驅動的錯誤,是由於seata server 默認不支持mysql 8。
能夠在seata server的 lib 文件夾下替換 mysql 的驅動 jar 包。lib 文件夾下,已經有一個 jdbc 文件夾,把裏面驅動版本爲 8 的 mysql-connector-java-x.jar 包拷貝到外面 lib 文件夾下便可。
github示例項目中包括3個業務服務、1個註冊中心,以及resources下的數據庫腳本和seata server配置文件。按照服務的啓動順序,以下分類:
3個業務服務中,order訂單服務
能夠被稱爲「主事務」,當訂單建立成功後,再在訂單服務中調用 account帳號服務
和 storage庫存服務
兩個「副事務」。所以從 seata tcc代碼層面上,能夠分紅下面兩類。
下文中不會列舉業務代碼,完整代碼能夠從github上查看,只會列出 seata 的相關代碼和配置。
配置文件中須要配置 tx-service-group
,須要注意的是,3個業務服務中都須要配置一樣的值。
application.yml
spring: cloud: alibaba: seata: tx-service-group: order_tx_group
在application.yml同級目錄,即 resources 目錄下,建立兩個seata 的配置文件。還記得在seata server 啓動的時候也有這兩個文件,但內容不同,不要混淆了。
file.conf
transport { type = "TCP" server = "NIO" heartbeat = true enableClientBatchSendRequest = true threadFactory { bossThreadPrefix = "NettyBoss" workerThreadPrefix = "NettyServerNIOWorker" serverExecutorThread-prefix = "NettyServerBizHandler" shareBossWorker = false clientSelectorThreadPrefix = "NettyClientSelector" clientSelectorThreadSize = 1 clientWorkerThreadPrefix = "NettyClientWorkerThread" bossThreadSize = 1 workerThreadSize = "default" } shutdown { wait = 3 } serialization = "seata" compressor = "none" } service { vgroupMapping.order_tx_group = "seata-server" order_tx_group.grouplist = "127.0.0.1:8091" enableDegrade = false disableGlobalTransaction = false } client { rm { asyncCommitBufferLimit = 10000 lock { retryInterval = 10 retryTimes = 30 retryPolicyBranchRollbackOnConflict = true } reportRetryCount = 5 tableMetaCheckEnable = false reportSuccessEnable = false } tm { commitRetryCount = 5 rollbackRetryCount = 5 } undo { dataValidation = true logSerialization = "jackson" logTable = "undo_log" } log { exceptionRate = 100 } }
registry.conf
registry { # file 、nacos 、eureka、redis、zk、consul、etcd三、sofa type = "eureka" eureka { serviceUrl = "http://localhost:8761/eureka" } } config { # file、nacos 、apollo、zk、consul、etcd三、springCloudConfig type = "file" file { name = "file.conf" } }
這是配置 TCC 子服務的核心代碼,
該註解須要添加到上面描述的接口上,表示實現該接口的類被 seata 來管理,seata 根據事務的狀態,自動調用咱們定義的方法,若是沒問題則調用 Commit 方法,不然調用 Rollback 方法。
該註解用在接口的 Try 方法上。
該註解用來修飾 Try 方法的入參,被修飾的入參能夠在 Commit 方法和 Rollback 方法中經過 BusinessActionContext 獲取。
在接口方法的實現代碼中,能夠經過 BusinessActionContext 來獲取參數, BusinessActionContext 就是 seata tcc 的事務上下文,用於存放 tcc 事務的一些關鍵數據。BusinessActionContext 對象能夠直接做爲 commit 方法和 rollbakc 方法的參數,Seata 會自動注入參數。
OrderTccAction.java
@LocalTCC public interface OrderTccAction { /** * try 嘗試 * * BusinessActionContext 上下文對象,用來在兩個階段之間傳遞數據 * BusinessActionContextParameter 註解的參數數據會被存入 BusinessActionContext * TwoPhaseBusinessAction 註解中commitMethod、rollbackMethod 屬性有默認值,能夠不寫 * * @param businessActionContext * @param orderNo * @param userId * @param productId * @param amount * @param money * @return */ @TwoPhaseBusinessAction(name = "orderTccAction") boolean prepareCreateOrder(BusinessActionContext businessActionContext, @BusinessActionContextParameter(paramName = "orderNo") String orderNo, @BusinessActionContextParameter(paramName = "userId") Long userId, @BusinessActionContextParameter(paramName = "productId") Long productId, @BusinessActionContextParameter(paramName = "amount") Integer amount, @BusinessActionContextParameter(paramName = "money") BigDecimal money); /** * commit 提交 * @param businessActionContext * @return */ boolean commit(BusinessActionContext businessActionContext); /** * cancel 撤銷 * @param businessActionContext * @return */ boolean rollback(BusinessActionContext businessActionContext); }
OrderTccActionImpl.java
@Slf4j @Component public class OrderTccActionImpl implements OrderTccAction { private final OrderMapper orderMapper; public OrderTccActionImpl(OrderMapper orderMapper){ this.orderMapper=orderMapper; } /** * try 嘗試 * * BusinessActionContext 上下文對象,用來在兩個階段之間傳遞數據 * BusinessActionContextParameter 註解的參數數據會被存入 BusinessActionContext * TwoPhaseBusinessAction 註解中commitMethod、rollbackMethod 屬性有默認值,能夠不寫 * * @param businessActionContext * @param orderNo * @param userId * @param productId * @param amount * @param money * @return */ @Transactional(rollbackFor = Exception.class) @Override public boolean prepareCreateOrder(BusinessActionContext businessActionContext, String orderNo, Long userId, Long productId, Integer amount, BigDecimal money) { orderMapper.save(new OrderDO(orderNo,userId, productId, amount, money, 0)); ResultHolder.setResult(OrderTccAction.class, businessActionContext.getXid(), "p"); return true; } /** * commit 提交 * * @param businessActionContext * @return */ @Transactional(rollbackFor = Exception.class) @Override public boolean commit(BusinessActionContext businessActionContext) { //檢查標記是否存在,若是標記不存在不重複提交 String p = ResultHolder.getResult(OrderTccAction.class, businessActionContext.getXid()); if (p == null){ return true; } /** * 上下文對象從第一階段向第二階段傳遞時,先轉成了json數據,而後還原成上下文對象 * 其中的整數比較小的會轉成Integer類型,因此若是須要Long類型,須要先轉換成字符串在用Long.valueOf()解析。 */ String orderNo = businessActionContext.getActionContext("orderNo").toString(); orderMapper.updateStatusByOrderNo(orderNo, 1); //提交完成後,刪除標記 ResultHolder.removeResult(OrderTccAction.class, businessActionContext.getXid()); return true; } /** * cancel 撤銷 * * 第一階段沒有完成的狀況下,沒必要執行回滾。由於第一階段有本地事務,事務失敗時已經進行了回滾。 * 若是這裏第一階段成功,而其餘全局事務參與者失敗,這裏會執行回滾 * 冪等性控制:若是重複執行回滾則直接返回 * * @param businessActionContext * @return */ @Transactional(rollbackFor = Exception.class) @Override public boolean rollback(BusinessActionContext businessActionContext) { //檢查標記是否存在,若是標記不存在不重複提交 String p = ResultHolder.getResult(OrderTccAction.class, businessActionContext.getXid()); if (p == null){ return true; } String orderNo = businessActionContext.getActionContext("orderNo").toString(); orderMapper.deleteByOrderNo(orderNo); //提交完成後,刪除標記 ResultHolder.removeResult(OrderTccAction.class, businessActionContext.getXid()); return true; } }
@GlobalTransactional
註解是惟一做用到「主事務」的方法。該註解加在「主事務」調用「副事務」的方法上。
OrderServiceImpl.java
@Service public class OrderServiceImpl implements OrderService { private final OrderTccAction orderTccAction; private final AccountFeign accountFeign; private final StorageFeign storageFeign; public OrderServiceImpl(OrderTccAction orderTccAction, AccountFeign accountFeign, StorageFeign storageFeign){ this.orderTccAction=orderTccAction; this.accountFeign=accountFeign; this.storageFeign=storageFeign; } /** * 建立訂單 * @param orderDO */ @GlobalTransactional @Override public void createOrder(OrderDO orderDO) { String orderNo=this.generateOrderNo(); //建立訂單 orderTccAction.prepareCreateOrder(null, orderNo, orderDO.getUserId(), orderDO.getProductId(), orderDO.getAmount(), orderDO.getMoney()); //扣餘額 accountFeign.decreaseMoney(orderDO.getUserId(),orderDO.getMoney()); //扣庫存 storageFeign.decreaseStorage(orderDO.getProductId(),orderDO.getAmount()); } private String generateOrderNo(){ return LocalDateTime.now() .format( DateTimeFormatter.ofPattern("yyMMddHHmmssSSS") ); } }
account 和 storage 兩個服務相比較於 order,只少了 「4.1.4. @GlobalTransactional 全局服務」,其餘的配置徹底同樣。所以,這裏就再也不贅言了。
測試
經過調用「主事務」 order-service 的建立訂單接口,來模擬分佈式事務。咱們能夠經過在3個業務服務的不一樣代碼處故意拋出錯誤,看是否可以實現事務的一致回滾。
seata框架表結構
在 /resources/database-sql 的數據庫腳本中,各自還有一些 seata 框架自己的表結構,用於存儲分佈式事務各自的中間狀態。由於這個中間狀態很短,一旦事務一致性達成,表數據就會自動刪除,所以平時咱們沒法查看數據庫。
由於seata tcc模式,會一直阻塞到全部的 try執行完畢,再執行後續的。從而咱們能夠經過在部分業務服務try的代碼中加上Thread.sleep(10000)
,強制讓事務過程變慢,從而就能夠看到這些 seata 表數據。
冪等性
tcc模式中,Commit
和 Cancel
都是有自動重試功能的,處於事務一致性考慮,重試功能頗有必要。但咱們就必定要慎重考慮方法的 冪等性
,示例代碼中的ResultHolder類並非個好方案,仍是要在Commit、Cancel業務方法自己作冪等性要求。