上篇的續集。java
工具:git
Idea201902/JDK11/Gradle5.6.2/Mysql8.0.11/Lombok0.27/Postman7.5.0/SpringBoot2.1.9/Nacos1.1.3/Seata0.8.1/SeataServer0.8.1/Dubbo2.7.3github
難度:新手--戰士--老兵--大師spring
目標:sql
1.使用Seata實現storage模塊的TCC模式的本地模式數據庫
2.使用Seata實現多級TCC模式apache
步驟:session
爲了更好的遇到各類問題,同時保持時效性,我儘可能使用最新的軟件版本。本項目代碼地址:其中的day18,https://github.com/xiexiaobiao/dubbo-project.gitmybatis
文中圖片有些顯示不全,是圖片很大,我擔憂縮放會看不清,因此部分顯示不全的,能夠下載圖片再看。架構
1.先說下TCC(Try-Confirm-Cancel)模式:
TCC模式即將每一個服務業務操做分爲兩個階段,第一個階段檢查並預留相關資源,可視爲一種臨時操做,第二階段根據全部服務業務的Try狀態來操做,若是都成功,則進行Confirm操做,若是任意一個Try發生錯誤,則所有Cancel,特徵在於它不依賴 RM 對分佈式事務的支持,而是經過對業務邏輯的分解來實現分佈式事務,不一樣於AT的是就是須要自行定義各個階段的邏輯,對業務有侵入。TCC使用要求就是業務接口都必須實現三段邏輯:
準備操做 Try:完成全部業務檢查,預留必須的業務資源。
確認操做 Confirm:真正執行的業務邏輯,不作任何業務檢查,只使用 Try 階段預留的業務資源。所以,只要 Try 操做成功,Confirm 必須能成功。另外,Confirm 操做需知足冪等性,保證一筆分佈式事務能且只能成功一次。
取消操做 Cancel:釋放 Try 階段預留的業務資源。一樣的,Cancel 操做也須要知足冪等性。
借用一張圖來解釋一下(此圖來源見文末參考文章):
try-先扣款30元 --> confirm-空操做 --> cancel-退回30元。
若是再優化一下,將更接近TCC的定義:
try-凍結30元,預留資源 --> confirm-扣款30元 --> cancel-退回30元。
2.項目延續自前篇文章,不變,僅修改了storage模塊,總體爲多模塊Dubbo微服務架構,目標一,即實現圖一中TCC模式。
3.先從com.biao.mall.storage.conf.SeataAutoConfig
開始:此類爲配置類,完成Seata框架依賴元素的注入,
先自動注入DataSourceProperties,獲取JDBC信息;
再注入一個GlobalTransactionScanner,用於掃描TCC事務;
@Configuration public class SeataAutoConfig { private DataSourceProperties dataSourceProperties; @Autowired public SeataAutoConfig(DataSourceProperties dataSourceProperties){ this.dataSourceProperties = dataSourceProperties; } /** * init durid datasource * @Return: druidDataSource datasource instance */ @Bean @Primary public DruidDataSource druidDataSource(){ DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setUrl(dataSourceProperties.getUrl()); druidDataSource.setUsername(dataSourceProperties.getUsername()); druidDataSource.setPassword(dataSourceProperties.getPassword()); druidDataSource.setDriverClassName(dataSourceProperties.getDriverClassName()); druidDataSource.setInitialSize(0); druidDataSource.setMaxActive(180); druidDataSource.setMaxWait(60000); druidDataSource.setMinIdle(0); druidDataSource.setValidationQuery("Select 1 from DUAL"); druidDataSource.setTestOnBorrow(false); druidDataSource.setTestOnReturn(false); druidDataSource.setTestWhileIdle(true); druidDataSource.setTimeBetweenEvictionRunsMillis(60000); druidDataSource.setMinEvictableIdleTimeMillis(25200000); druidDataSource.setRemoveAbandoned(true); druidDataSource.setRemoveAbandonedTimeout(1800); druidDataSource.setLogAbandoned(true); return druidDataSource; } /** * init datasource proxy * @Param: druidDataSource datasource bean instance * @Return: DataSourceProxy datasource proxy */ @Bean public DataSourceProxy dataSourceProxy(DruidDataSource druidDataSource){ return new DataSourceProxy(druidDataSource); } /** * init mybatis sqlSessionFactory * @Param: dataSourceProxy datasource proxy * @Return: DataSourceProxy datasource proxy */ @Bean public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSourceProxy); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:/mapper/*Mapper.xml")); factoryBean.setTransactionFactory(new JdbcTransactionFactory()); return factoryBean.getObject(); } /** * init global transaction scanner * @Return: GlobalTransactionScanner */ @Bean public GlobalTransactionScanner globalTransactionScanner(){ return new GlobalTransactionScanner("${spring.application.name}", "my_test_tx_group"); } }
4.核心之一com.biao.mall.storage.service.ProductService
接口:
@LocalTCC,註解標識此TCC爲本地模式:即該事務是本地調用,非RPC調用;
@TwoPhaseBusinessAction,註解標識爲TCC模式,其中定義了commitMethod 和rollbackMethod
@LocalTCC public interface ProductService extends IService<ProductEntity> { /** * 扣減庫存 */ // ObjectResponse decreaseStorage(CommodityDTO commodityDTO); /** TCC 模式 */ @TwoPhaseBusinessAction(name = "StorageAction",commitMethod = "commit",rollbackMethod = "rollback") boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "commodityDTO") CommodityDTO commodityDTO); boolean commit(BusinessActionContext actionContext); boolean rollback(BusinessActionContext actionContext); }
5.com.biao.mall.storage.impl.ProductServiceImpl
,上面接口的實現類:try階段執行業務邏輯,commit階段空操做,rollback執行回退,看官固然能夠實現一個優化的方案:try階段執行數量凍結邏輯,commit階段真實提交操做,rollback執行回退;
@Service public class ProductServiceImpl extends ServiceImpl<ProductDao, ProductEntity> implements ProductService { @Override public boolean prepare(BusinessActionContext actionContext, CommodityDTO commodityDTO) { System.out.println("actionContext獲取Xid prepare>>> "+actionContext.getXid()); System.out.println("actionContext獲取TCC參數 prepare>>> "+actionContext.getActionContext("commodityDTO")); int storage = baseMapper.decreaseStorage(commodityDTO.getCommodityCode(), commodityDTO.getCount()); //測試rollback時打開 /*int a = 1/0; System.out.println(a);*/ if (storage > 0){ return true; } return false; } @Override public boolean commit(BusinessActionContext actionContext) { System.out.println("actionContext獲取Xid commit>>> "+actionContext.getXid()); return true; } @Override public boolean rollback(BusinessActionContext actionContext) { System.out.println("actionContext獲取Xid rollback>>> "+actionContext.getXid()); //必須注意actionContext.getActionContext返回的是Object,且不可以使用如下語句直接強轉! //CommodityDTO commodityDTO = (CommodityDTO) actionContext.getActionContext("commodityDTO"); CommodityDTO commodityDTO = JSONObject.toJavaObject((JSONObject)actionContext.getActionContext("commodityDTO"),CommodityDTO.class); int storage = baseMapper.increaseStorage(commodityDTO.getCommodityCode(), commodityDTO.getCount()); if (storage > 0){ return true; } return false; } }
對於以上代碼,rollback方法中,必須注意actionContext.getActionContext返回的是Object,且不可以使用如下語句直接強轉,運行會報錯!
CommodityDTO commodityDTO = (CommodityDTO)actionContext.getActionContext("commodityDTO");
6.com.biao.mall.storage.controller.ProductController
,寫個簡單的API入口:
@RestController @RequestMapping("/product") public class ProductController { @Autowired private ProductService productService; private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); @PostMapping("/dec") @GlobalTransactional public String handleBusiness(@RequestBody CommodityDTO commodityDTO) { LOGGER.info("請求參數:{}", commodityDTO.toString()); LOGGER.info("全局XID:{}", RootContext.getXID()); ObjectResponse<Object> response = new ObjectResponse<>(); if (productService.prepare(null,commodityDTO)){ return "success"; }; return null; } }
7.成功Commit測試:依次啓動:Nacos-->SeataServer-->storage
Postman提交數據:
後臺輸出,注意數字標識:
得到請求參數對象DTO(1)
Seata開啓全局事務(9)
Try階段處理(2,3)
標識事務模式爲TCC(4)
Commit階段處理(5,6)
第二階段成功(7),全局事務成功(8)
8.rollback測試:
打開com.biao.mall.storage.impl.ProductServiceImpl
中prepare方法中的異常測試代碼,再次運行:
Seata開啓全局事務(1)
Rollback階段處理(2)
第二階段回退成功(3),全局事務成功(4)
目標一達成!
9.目標二,目前暫時沒辦法在該項目框架下實現,因基於@Reference
註解獲取的Dubbo服務,在Seata框架下,分支事務中BusinessActionContext
實例一直是Null,折騰一成天,發現這是Seata的一個未關閉的Issue,做罷,之後再寫!替代方案是基於XML方式,註冊和獲取Dubbo服務,再使用Spring模式的ApplicationContext獲取服務Bean,是能夠正常使用的,但我以爲這套方案不是將來的方向,過於原始,故放棄了,看官能夠嘗試!
覆盤記:
1.SeataServer,即TC,其安裝目錄下文件 \seata\bin\sessionStore\root.data
會持久化事務執行狀態,經測試,若是提交階段失敗,即將storage/src/main/java/com/biao/mall/storage/impl/ProductServiceImpl.java
中commit空提交改成返回false,發生錯誤重啓TC或應用,會自動繼續嘗試commit提交,再重啓應用,RM註冊失敗,重啓SeataServer,也會自動繼續commit,見下圖,說明有進行文件形式的持久化機制!爲啥?由於對於二階段式提交,只要try成功,commit是必需要成功的(或者try成功後,rollback必定要成功),如不這樣,數據從理論上就是處於半狀態,這系列動做原本就是一個事務,故由TC來保證此原則的執行。要強行恢復正常,手動刪掉root.data便可!
2.後臺的紅色告警:
使用依賴樹分析:
更新依賴便可消除,神奇的是org.apache.dubbo:dubbo:2.7.3中,我測試3.24,3.25,3.26都會報警告,只有3.23.1-GA剛恰好,這口味真獨特,過老過嫩都不要!
compile group: 'org.javassist', name: 'javassist', version: '3.23.1-GA'
3.事實上com.biao.mall.storage.conf.SeataAutoConfig中:能夠將DataSourceProxy不進行DI,執行效果以下圖,能夠看到sql的過程了,由於此時是由Spring來操做的,DataSourceProxy配置是AT模式必須的,由於要由Seata來代理完成數據操做,這也是TCC和AT模式的一大區別!
4.參考文章:https://juejin.im/post/5cbfd9a26fb9a03212505785
推薦閱讀: