1. 程式人生 > >Dubbo學習系列之十五(Seata分散式事務方案TCC模式)

Dubbo學習系列之十五(Seata分散式事務方案TCC模式)

上篇的續集。

工具:

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.3

難度:新手--戰士--老兵--大師

目標:

1.使用Seata實現storage模組的TCC模式的本地模式

2.使用Seata實現多級TCC模式


步驟:

為了更好的遇到各種問題,同時保持時效性,我儘量使用最新的軟體版本。本專案程式碼地址:其中的day18,https://github.com/xiexiaobiao/dubbo-project.git

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


 

推薦閱讀:

  • Dubbo學習系列之十四(Seata分散式事務方案AT模式)

  • Dubbo學習系列之十三(Mycat資料庫代理)

  • Dubbo學習系列之十二(Quartz任務排程)