当前位置:网站首页>seata 1.3.0 四種模式解决分布式事務(AT、TCC、SAGA、XA)

seata 1.3.0 四種模式解决分布式事務(AT、TCC、SAGA、XA)

2022-07-07 10:50:00 鐺鐺響

前言

1、seata版本 1.3.0

2、基礎項目結構,大家只需要關注 設備模塊 device工單模塊 order即可。
在這裏插入圖片描述-

項目說明
api-gateway網關模塊
common基礎模塊
device設備模塊
order工單模塊
user用戶模塊

3、數據庫說明, 設備模塊 device鏈接gxm-301數據庫,工單模塊 order鏈接 gxm-300數據庫
在這裏插入圖片描述

4、主要業務說明,在生成工單的時候,我們使用order服務向 gxm-300數據庫的錶work-ordernotice_info插入數據庫,並且遠程調用device服務插入gxm-301數據庫的錶 work-problem 和錶work_order_problem_link

5、調試說明,我們在使用@GlobalTransactional注解的時候,seata的控制事務是有時間限制的默認為1分鐘,所以在我們debug的時候如果時間過長,seata就默認回滾了,為了方便大家調試,可以修改這個參數。

在這裏插入圖片描述

6、官方的==新人文檔 是一定要看的==

一、AT 模式

1、對於seata 來說默認開啟的就是 AT模式,而且如果你依賴 seata-spring-boot-starter 時,自動代理數據源,無需額外處理

2、對於AT模式在回滾的時候會找到 undo_log 中的前鏡像與後鏡像,來進行恢複。

但是在恢複的時候,會比較後鏡像是否有沒有被修改過,即進行數據進行比較,如果有不同,說明數據被當前全局事務之外的動作進行了修改,這個時候AT模式做數據校驗的時候,會回滾失敗,因為校驗不通過,我們也可以通過配置參數關閉這個前後鏡像的校驗過程,不過這個是非常不建議的,因為,被其他線程修改了導致不能還原現場這種情况,確實還是需要人為去處理的

3、這是官方的的AT模式的使用說明,這都是必須要注意的點偶。
在這裏插入圖片描述

4、其中官方的 新人文檔 是一定要看的,其中在老版本中,我們是要代理數據源的,如下,具體的模式選擇不同的數據源來代理即可,比如我們現在模擬的是AT,那就是return new DataSourceProxy(druidDataSource);

@Primary
@Bean("dataSource")
public DataSource dataSource(DataSource druidDataSource) {
    
    //AT 代理 二選一
    return new DataSourceProxy(druidDataSource);
    //XA 代理
    return new DataSourceProxyXA(druidDataSource)
}

5、但是如何你使用的是高版本的或者使用的是 seata-starter,就不用手動配置,因為我使用的是 seata-starter,而且我現在演示的是AT模式,所以不用改什麼(後續在XA模式的時候會去修改)
在這裏插入圖片描述
在這裏插入圖片描述

6、常見的問題,官方已經有了說明和回答 常見問題,在用於生產之前,這些最好都看一遍。

1.1、使用說明

1.1.1、使用

1、我們使用注解 @GlobalTransactional開啟seataAT模式的事務管理,而且因為是使用的是seata-starter,那這個注解會自動代理AT模式的數據源,具體代碼如下,可以看到代碼主要分為兩部分,第一部分是調用自己order服務的2個錶的mapper 插入數據到gxm-300,第二部分是遠程調用device的兩個mapper來進行插入數據到gxm-301
在這裏插入圖片描述

在這裏插入圖片描述

@GlobalTransactional(name = "default", rollbackFor = Exception.class)
    @Override
    public R saveWithDetail(SaveWithDetailDTO saveWithDetailDTO) {
    
        log.info("create order begin ... xid: " + RootContext.getXID());

        String title = RandomUtil.randomString(20);
        saveWithDetailDTO.setWorkOrderTitle(title);
        saveWithDetailDTO.setWorkOrderNumber("asd");

        // 1、調用自身服務
        // 1.1、插入工單信息
        WorkOrder workOrder = new WorkOrder();
        BeanUtils.copyProperties(saveWithDetailDTO, workOrder);
        this.baseMapper.insert(workOrder);

        // 1.2、插入消息通知錶
        NoticeInfo noticeInfo = new NoticeInfo();
        noticeInfo.setTitle("new work order 【" + title + "】has publish");
        noticeInfoMapper.insert(noticeInfo);

        // 當工單id不為null時,模擬一個异常
        if (saveWithDetailDTO.getId() != null) {
    
            int i = 1 / 0;
        }

        // 2、遠程調用 device 服務
        // 2.1、插入問題錶 和問題關聯錶
        WorkProblemDTO workProblemDTO = new WorkProblemDTO();
        BeanUtils.copyProperties(saveWithDetailDTO.getSoftwareNotSolveProblemList().get(0), workProblemDTO);
        workProblemDTO.setOrderId(workOrder.getId());
        workProblemApi.insertWithLink(workProblemDTO);
        return R.ok();
    }

在這裏插入圖片描述

@Override
    public R insertWithLink(WorkProblemDTO workProblemDTO) {
    
        WorkProblem workProblem = new WorkProblem();
        BeanUtils.copyProperties(workProblemDTO, workProblem);
        // 1、插入問題錶
        int insertProblem = workProblemMapper.insert(workProblem);

        // 2、插入工單問題關聯錶
        WorkOrderProblemLink workOrderProblemLink = new WorkOrderProblemLink();
        workOrderProblemLink.setOrderId(workProblemDTO.getOrderId());
        workOrderProblemLink.setProblemId(workProblem.getId());
        int insertOrderProblemLink = workOrderProblemLinkMapper.insert(workOrderProblemLink);

        if (insertProblem > 0 && insertOrderProblemLink > 0) {
    
            return R.ok();
        }
        throw new RuntimeException("插入异常");
    }

2、先測試成功的方式,即傳參的時候id為空,則2個數據庫的4張錶都沒有問題,都插入成功,說明沒有問題

3、再測試不成功,即傳參的時候id不為空,則seata數據全局事務就會生效,2個數據庫4張錶都沒有數據庫,說明seataAT模式生效了,

1.1.2、刨析

1、我們打個斷點,可以就可以發現AT模式的秘密所在了,我們直接在調用鏈的最後的比特置打上斷點,這個比特置是4個mapper都已經插入成功了,但是device服務沒有返回,所以整個鏈路沒有結束,並且此時我加長了事務的時間,足够我們調試了。

在這裏插入圖片描述
2、在端點處停止的時候,我們觀察gxm-300數據庫和gxm-301數據庫,你會發現,4個mapper的插入數據都已經插入到數據庫了,並且一個mapper會在對應的undo_log錶中插入一條數據,其中會有前置鏡像數據和後置鏡像數據,以及分支id branch_id

3、gxm-300 的 undo_log

id	branch_id	xid	context	rollback_info	log_status	log_created	log_modified	ext
7	278197152412237825	192.168.172.232:8091:278197152336740352	serializer=jackson	(BLOB) 3.66 KB	0	2022-06-09 16:16:10	2022-06-09 16:16:10	
8	278197152475152385	192.168.172.232:8091:278197152336740352	serializer=jackson	(BLOB) 2.27 KB	0	2022-06-09 16:16:10	2022-06-09 16:16:10	

在這裏插入圖片描述
4、 gxm-301 的 undo_log

id	branch_id	xid	context	rollback_info	log_status	log_created	log_modified	ext
7	278197152550649857	192.168.172.232:8091:278197152336740352	serializer=jackson	(BLOB) 982 bytes	0	2022-06-09 16:16:10	2022-06-09 16:16:10	
8	278197152600981505	192.168.172.232:8091:278197152336740352	serializer=jackson	(BLOB) 999 bytes	0	2022-06-09 16:16:10	2022-06-09 16:16:10	

在這裏插入圖片描述

5、seata 的 branch_table

branch_id	xid	transaction_id	resource_group_id	resource_id	branch_type	status	client_id	application_data	gmt_create	gmt_modified
278197152412237825	192.168.172.232:8091:278197152336740352	278197152336740352		jdbc:mysql://127.0.0.1:3306/gxm-300 AT 0 OrderApplication-seata-id:192.168.172.232:56035 2022-06-09 16:16:09.791745 2022-06-09 16:16:09.791745
278197152475152385	192.168.172.232:8091:278197152336740352	278197152336740352		jdbc:mysql://127.0.0.1:3306/gxm-300 AT 0 OrderApplication-seata-id:192.168.172.232:56035 2022-06-09 16:16:09.806598 2022-06-09 16:16:09.806598
278197152550649857	192.168.172.232:8091:278197152336740352	278197152336740352		jdbc:mysql://127.0.0.1:3306/gxm-301 AT 0 DeviceApplication-seata-id:192.168.172.232:56409 2022-06-09 16:16:09.824506 2022-06-09 16:16:09.824506
278197152600981505	192.168.172.232:8091:278197152336740352	278197152336740352		jdbc:mysql://127.0.0.1:3306/gxm-301 AT 0 DeviceApplication-seata-id:192.168.172.232:56409 2022-06-09 16:16:09.837013 2022-06-09 16:16:09.837013

在這裏插入圖片描述

6、 seata 的 global_table

xid	transaction_id	status	application_id	transaction_service_group	transaction_name	timeout	begin_time	application_data	gmt_create	gmt_modified
192.168.172.232:8091:278197152336740352	278197152336740352	5	OrderApplication-seata-id	my_test_tx_group	default	600000	1654762569770		2022-06-09 16:16:09	2022-06-09 16:16:42

在這裏插入圖片描述
7、seata 的 lock_table

row_key	xid	transaction_id	branch_id	resource_id	table_name	pk	gmt_create	gmt_modified
jdbc:mysql://127.0.0.1:3306/gxm-300^^^notice_info^^^4 192.168.172.232:8091:278197152336740352 278197152336740352 278197152475152385 jdbc:mysql://127.0.0.1:3306/gxm-300 notice_info 4 2022-06-09 16:16:09 2022-06-09 16:16:09
jdbc:mysql://127.0.0.1:3306/gxm-300^^^work_order^^^4 192.168.172.232:8091:278197152336740352 278197152336740352 278197152412237825 jdbc:mysql://127.0.0.1:3306/gxm-300 work_order 4 2022-06-09 16:16:09 2022-06-09 16:16:09
jdbc:mysql://127.0.0.1:3306/gxm-301^^^work_order_problem_link^^^4 192.168.172.232:8091:278197152336740352 278197152336740352 278197152600981505 jdbc:mysql://127.0.0.1:3306/gxm-301 work_order_problem_link 4 2022-06-09 16:16:09 2022-06-09 16:16:09
jdbc:mysql://127.0.0.1:3306/gxm-301^^^work_problem^^^4 192.168.172.232:8091:278197152336740352 278197152336740352 278197152550649857 jdbc:mysql://127.0.0.1:3306/gxm-301 work_problem 4 2022-06-09 16:16:09 2022-06-09 16:16:09

在這裏插入圖片描述
8、關聯關系就是branch_idtransaction_id,一個 transaction_id 錶示一次全局事務的開始,旗下會有多個branch_id分支事務

9、如果我們的業務最後沒有問題(指的是業務正常插入成功,或者有异常但是seata的AT模式幫你回滾了,而且回滾的時候沒有任何問題),那麼這些錶都不會有數據,因為我們的全局事務結束了,保證的當次業務的流程了,即使是失敗了,但是幫你回滾了。一旦我們的錶有數據,就說明,業務執行發生了异常而seata回滾的時候發現有問題,這個時候,seata就會把相關信息的錶數據存儲起來,不删除,我們看到就要去處理了。

比如一種情况,我們在异常流程中,第一個線程執行一半的時候,即work_order錶數據插入成功了,但是我們在數據庫手動修改,或者其他線程事務修改了這條剛生產的數據,但第一個現場執行到後面,即准備插入work_problem發生了异常,那麼這個時候,seata的at模式會根據相關日志來進行回滾,但是回滾的時候,它會檢查,在這期間`work_order``那條剛插入的數據,有沒有被修改,一旦和它當初記錄的不一致,那麼它iu沒法幫你處理了。這個時候,相關錶的數據就存儲下來了,我們就要根據這些信息來手動處理了。

1.1.3、AT模式回滾失敗,處理

1、對於前面的1.1.2節的調試,我這裏出現了問題,可能是因為我斷點停留時間太長了,會發現相關錶有數據,說seata的at模式回滾失敗了。接下來我們就要去處理了。

2、看到了全局事務id278197152336740352

在這裏插入圖片描述
3、找當前全局事務下有那些事務分支遺留了下來,可以看到是gxm-300的業務有問題,而且pk字段是4,說明是這兩種錶的主鍵為4的有問題。

在這裏插入圖片描述
4、於是我們到對應的數據中找到它的undo_log,可以看到對應的分支id也是和前面的對應的上的,其中rollback_info字段記錄的就是前置鏡像的數據和後置鏡像的數據
在這裏插入圖片描述
5、我們點擊rollback_info字段,然後保存數據為 xxx.json文件,打開如下

{
    
    "@class":"io.seata.rm.datasource.undo.BranchUndoLog",
    "xid":"192.168.172.232:8091:278197152336740352",
    "branchId":278197152412237825,
    "sqlUndoLogs":[
        "java.util.ArrayList",
        [
            {
    
                "@class":"io.seata.rm.datasource.undo.SQLUndoLog",
                "sqlType":"INSERT",
                "tableName":"work_order",
                "beforeImage":{
    
                    "@class":"io.seata.rm.datasource.sql.struct.TableRecords$EmptyTableRecords",
                    "tableName":"work_order",
                    "rows":[
                        "java.util.ArrayList",
                        [

                        ]
                    ]
                },
                "afterImage":{
    
                    "@class":"io.seata.rm.datasource.sql.struct.TableRecords",
                    "tableName":"work_order",
                    "rows":[
                        "java.util.ArrayList",
                        [
                            {
    
                                "@class":"io.seata.rm.datasource.sql.struct.Row",
                                "fields":[
                                    "java.util.ArrayList",
                                    [
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"id",
                                            "keyType":"PRIMARY_KEY",
                                            "type":4,
                                            "value":4
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"work_order_number",
                                            "keyType":"NULL",
                                            "type":12,
                                            "value":"asd"
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"work_order_title",
                                            "keyType":"NULL",
                                            "type":12,
                                            "value":"bfkzc4oganhbirygfd87"
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"client_name",
                                            "keyType":"NULL",
                                            "type":12,
                                            "value":"袁玉環2"
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"client_contact",
                                            "keyType":"NULL",
                                            "type":12,
                                            "value":"騰訊333"
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"client_phone",
                                            "keyType":"NULL",
                                            "type":12,
                                            "value":"181562383652"
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"order_service_type",
                                            "keyType":"NULL",
                                            "type":4,
                                            "value":1
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"order_type",
                                            "keyType":"NULL",
                                            "type":4,
                                            "value":1
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"service_type",
                                            "keyType":"NULL",
                                            "type":4,
                                            "value":3
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"deal_user_id",
                                            "keyType":"NULL",
                                            "type":4,
                                            "value":20
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"order_content",
                                            "keyType":"NULL",
                                            "type":-1,
                                            "value":"周末晚上聚會2"
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"create_user_id",
                                            "keyType":"NULL",
                                            "type":4,
                                            "value":null
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"create_time",
                                            "keyType":"NULL",
                                            "type":93,
                                            "value":null
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"specify_processing_day",
                                            "keyType":"NULL",
                                            "type":91,
                                            "value":[
                                                "java.sql.Date",
                                                1653321600000
                                            ]
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"receive_time",
                                            "keyType":"NULL",
                                            "type":93,
                                            "value":null
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"receiver_submit_time",
                                            "keyType":"NULL",
                                            "type":93,
                                            "value":null
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"end_time",
                                            "keyType":"NULL",
                                            "type":93,
                                            "value":null
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"order_status",
                                            "keyType":"NULL",
                                            "type":4,
                                            "value":null
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"receiver_refuse_content",
                                            "keyType":"NULL",
                                            "type":-1,
                                            "value":null
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"receiver_deal_content",
                                            "keyType":"NULL",
                                            "type":-1,
                                            "value":null
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"receiver_result_status",
                                            "keyType":"NULL",
                                            "type":4,
                                            "value":null
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"send_refuse_content",
                                            "keyType":"NULL",
                                            "type":12,
                                            "value":null
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"device_model",
                                            "keyType":"NULL",
                                            "type":12,
                                            "value":null
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"device_number",
                                            "keyType":"NULL",
                                            "type":12,
                                            "value":null
                                        },
                                        {
    
                                            "@class":"io.seata.rm.datasource.sql.struct.Field",
                                            "name":"service_report_images",
                                            "keyType":"NULL",
                                            "type":-1,
                                            "value":"http://gxm-tensquare.oss-cn-beijing.aliyuncs.com/2022-04/25/e214ba1b-6541-4756-8a2b-fdb1c29111e4.jpg"
                                        }
                                    ]
                                ]
                            }
                        ]
                    ]
                }
            }
        ]
    ]
}

6、根據上述的json文件內容,直接操作是insert,而device沒有插入成功,但是它插入成功了,我們把相關的數據删除即可,後續如果是其他情况,比如update這種,根據前後鏡像數據,按照需求處理。

1.2、測試相應方法上不放置spring的事務注解,多個業務是否正常回滾

1、對於上述的方法,我們知道每個order服務調用了本地的2個mapper,插入到他鏈接的數據庫(gxm-300)中,然後遠程調用deivce服務的方法,而device的方法裏面是調用device本地的2個mapper,插入到他鏈接的數據庫(gxm-301)中,所以,我們不在每個方法上面加上spring的事務注解,即不在order服務的saveWithDetail方法和device服務的insertWithLink方法上使用spring的事務注解
在這裏插入圖片描述

在這裏插入圖片描述
2、這個結果其實在我們的第一節中簡單的使用中就已經證明了,是可以的,我們不需要再加上對應的spring的事務注解了,seata 會保證的。

3、當然有人可能會說,對於下面這個方法,也許device本身的業務需要用到,用的不是分布式事務,那需要spring 來管理,我需要加上spring的事務處理注解,這個說法呢,你當然可以加上spring的事務注解,seata不會影響,但是我不建議,因為這個類下的api就是提供給外部調用的,如果是內部本服務的業務應該在其他類中去處理,不應該放在這裏,被其內部調用是不合適的。

在這裏插入圖片描述

1.3、測試seata回滾時,鏡像數據被其他事務修改後,無法回滾成功的情况

1、首先大家要了解seata的AT模式流程,官方文檔:Seata AT 模式

2、看完這個流程之後,大家就能腦補出一個問題,就是如下在全局事務未提交的情况下,鏡像數據被其他現場修改了,如下,這種情况下seata是沒有辦法處理,除非你關閉前後鏡像檢查,强制數據更新,這太不穩妥了。

訂單服務和庫存服務。
開啟全局事務後,庫存服務已提交本地事務,50庫存修改為49,全局事務未提交。
另外一個線程開啟本地事務,修改庫存從49到48。
然而訂單服務報錯,全局事務需要回滾,這時會全局事務會回滾失敗,出現髒數據。
有什麼好的方案去處理這種情况嗎?或者避免這種情况的發生

3、這種問題,github的issue已經提出了 髒寫導致數據回滾失敗?,這種情况,分兩種處理方式

4、這篇官方文章也說的很好 詳解 Seata AT 模式事務隔離級別與全局鎖設計,也說到了髒寫的情况和處理。

1.3.1、模擬這種情况

1、還是之前的接口,我們修改seata的全局控制時間,因為等下測試時間會很長
在這裏插入圖片描述

2、我們在device服務中在執行玩所有的mapper後停頓20秒,此時數據庫就已經有2個數據庫的4個錶的數據了,如下

在這裏插入圖片描述

3、注意這裏我們設置的時間很長,所以遠程調用可能會出現超時了,直接導致seata回滾,我們還未來得急調用修改的線程呢,所以,我們需要修改遠程調用的組件的超時時間,我這裏用的是dubbo,所以我設置消費者order的dubbo的調用超時時間即可,修改為30秒,足够了。 springBoot集成dubbo的超時時間設置

# 設置遠程調用的超時時間和重試次數
dubbo.provider.timeout=30000
dubbo.provider.retries=0
dubbo.consumer.timeout=30000
dubbo.consumer.retries=0

在這裏插入圖片描述

4、增加一個線程去修改未提交的數據,在前面停頓的在20秒的時候,我們去調用如下接口,把為提交事務的數據修改掉。
在這裏插入圖片描述
5、我們另一個線程修改的是device服務的數據,所以,可以看到控制臺日志如下,而且rm此時會一直嘗試,可以看後面的那張圖,一直嘗試一直失敗,控制臺一直打印嘗試失敗的信息。
在這裏插入圖片描述
在這裏插入圖片描述

6、數據層面如下,所以數據庫gxm-300gxm-301的undo_log的數據條數加起來一共一定是3條。
在這裏插入圖片描述
在這裏插入圖片描述

1.3.2、第一種處理方式(手動處理,根據業務挽救)

7、我們可以根據上述的錶情况數據,來手動根據業務處理,比如通過lock_table的字段pk,和當前業務知道,多插入了這三個錶的三條數據,主鍵都是為1的,所以根據我們的業務我們直接删除相關數據即可。然後記得seata數據庫的相關錶的數據也得删除偶,以及對應的undo_log錶數據也需要删除。

在這裏插入圖片描述

1.3.3、第二種處理方式(@GlobalLock)

1、在本地修改的事務上加上@GlobalLock

  • 其中參數 lockRetryTimes 嘗試間隔時間,lockRetryTimes嘗試次數,說明在多少秒內間隔多少次會不斷重試獲取全局鎖,如果該記錄在全局事務中,則會失敗
  • 這兩個參數是在1.4.0和其以上版本才出現的,1.3.0m還沒有。
    在這裏插入圖片描述

2、可以參考 Seata入門系列(22)[email protected]注解使用場景及源碼分析,說的很好

3、另一個修改的線程發現修改的數據在全局事務中,所以不支持修改。

在這裏插入圖片描述
4、事務回滾成功,undo_log 的前置鏡像數據和數據庫的數據保持一致,說明沒有被之前的那個線程修改掉。
在這裏插入圖片描述

在這裏插入圖片描述

二、TCC 模式

1、其實TCC模式和AT流程上來說是一樣的,只是AT是自動根據undo_log來進行事務回滾和補償,而TCC則需要我們提供相應的接口,官方也都錶明了 Seata TCC 模式,可以看到TCC的第一階段和第二階段都是自定義的邏輯,seata只管在特定情况下調用。而AT就是全靠undo_log,然後seata判斷來幫你處理。

在這裏插入圖片描述

2、這裏需要介紹幾個後面需要用到的基礎注解和參數

  • @LocalTCC 適用於SpringCloud+Feign模式下的TCC,但是當我實驗的時候,調用使用的是dubbo,理論上是不用這個注解的(官方的demo中用dubbo的也沒有加這個注解),但是我試了一下,不加就會出現 tcc BusinessActionContext get null ,官方到現在還未處理,不知道是什麼問題,我在下面也回複了。
    在這裏插入圖片描述

  • @TwoPhaseBusinessAction 注解try方法,其中name為當前tcc方法的bean名稱,寫方法名便可(記得全局唯一),commitMethod指向提交方法,rollbackMethod指向事務回滾方法。指定好三個方法之後,seata會根據全局事務的成功或失敗,去幫我們自動調用提交方法或者回滾方法。

  • @BusinessActionContextParameter 注解可以將參數傳遞到二階段(commitMethod/rollbackMethod)的方法,這個也是下面提到的問題,第二階段獲取的參數只能是第一階段的一開始通過注解定義的參數值,即使你在第一階段修改,添加,也沒法在第二階段獲取到最新的參數值。

  • BusinessActionContext 便是指TCC事務上下文,可以通過該參數獲取xidbranchIdactionName,以及一些參數,注意,這裏有個問題就是 於prepare階段,也就是try階段代碼的數據添加參數,或者修改參數,在confrim和cancel階段的方法裏面是接受不到你修改後的數據的。

3、TCC 參與者需要實現三個方法,分別是一階段 Try 方法、二階段 Confirm 方法以及二階段 Cancel 方法。在 TCC 參與者的接口中需要先加上 @TwoPhaseBusinessAction 注解,並聲明這個三個方法,如下所示

public interface TccAction {
    
    @TwoPhaseBusinessAction(name = "yourTccActionName", commitMethod = "confirm", rollbackMethod = "cancel")
    public boolean try(
    BusinessActionContext businessActionContext, int a, int b);

    public boolean confirm(BusinessActionContext businessActionContext);

    public boolean cancel(BusinessActionContext businessActionContext);
}

@TwoPhaseBusinessAction 注解屬性說明:

  • name :TCC參與者的名稱,可自定義,但必須全局唯一。

  • commitMethod:指定二階段 Confirm 方法的名稱,可自定義。

  • rollbackMethod:指定二階段 Cancel 方法的名稱,可自定義。

4、TCC 方法參數說明:

  • Try:第一個參數類型必須是BusinessActionContext,後續參數的個數和類型可以自定義。

  • Confirm:有且僅有一個參數,參數類型必須是 BusinessActionContext,後續為相應的參數名(businessActionContext)。

  • Cancel:有且僅有一個參數,參數類型必須是 BusinessActionContext,後續為相應的參數名(businessActionContext)。

5、TCC 方法返回類型說明:

  • 一階段的 Try 方法可以為 boolean 類型,也可以自定義返回值。

  • 二階段的 Confirm 和 Cancel 方法的返回類型必須為 boolean 類型。

6、各接口作用:(下面的demo實際上並沒有嚴格按照這個方式來執行,建議生產環境按照如下步驟保證,要建立一張資源預留錶用於鎖住資源,可以參考這篇文章,原生TCC實現)

可以參考demo,原生TCC實現 https://github.com/prontera/spring-cloud-rest-tcc/tree/readme-img,裏面就建立了一張資源錶,用於try階段,預留資源。

  • Try:初步操作。完成所有業務檢查,預留必須的業務資源。(比如select for update 鎖住某條記錄,預留指定資源)

  • Confirm:確認操作。真正執行的業務邏輯(比如根據try的數據,更新庫存之類的操作),不作任何業務檢查,只使用 Try 階段預留的業務資源。因此,只要 Try 操作成功, Confirm 必定能成功。另外,Confirm 操作需滿足幂等性,保證一筆分布式事務能且只能成功一次。

  • Cancel:取消操作。釋放 Try 階段預留的業務資源。同樣的,Cancel 操作也需要滿足幂等性

2.1、代碼模擬

2.1.1、業務service

1、還是上面的基礎項目,不過需要稍微改動一下,我們抽取一個專門處理複雜業務的service類出來,裏面分別調用order服務和device服務,這樣看著清楚一些,如下,在調用的時候,BusinessActionContext 參數,我們傳null即可,seata會為其賦值的。

在這裏插入圖片描述

@GlobalTransactional(name = "default", rollbackFor = Exception.class, timeoutMills = 60000 * 10)
    @Override
    public R saveWithDetail(SaveWithDetailDTO saveWithDetailDTO) {
    
        log.info("create order begin ... xid: " + RootContext.getXID());

        // 1、order 服務
        workOrderService.simpleSave(null, saveWithDetailDTO);
        
        
        // 2、遠程調用 device 服務
        // 2.1、插入問題錶 和問題關聯錶
        WorkProblemDTO workProblemDTO = new WorkProblemDTO();
        BeanUtils.copyProperties(saveWithDetailDTO.getSoftwareNotSolveProblemList().get(0), workProblemDTO);
        workProblemDTO.setOrderId(saveWithDetailDTO.getId());
        workProblemApi.insertWithLink(null, workProblemDTO);
        return R.ok();
    }

2、异常模擬我們放在device服務中
在這裏插入圖片描述

2.1.2、order服務

1、注意我們要在接口上加上注解@LocalTCC,開啟tcc事務,並在第一階段的方法上加上注解@TwoPhaseBusinessAction,並賦值注解的值,錶明第二階段的commitrollback方法分別是什麼,以及三個方法的返回值得是boolean

@LocalTCC
public interface WorkOrderService extends IService<WorkOrder> {
    


    String simpleSave_BusinessActionContextParameter = "saveWithDetailDTO";

    /** * 增加工單 * * @param saveWithDetailDTO saveWithDetailDTO */
    @TwoPhaseBusinessAction(name = "DubboTccSimpleSaveActionOne", commitMethod = "simpleSaveCommit", rollbackMethod = "simpleSaveRollback")
    boolean simpleSave(BusinessActionContext actionContext,
                       @BusinessActionContextParameter(paramName = simpleSave_BusinessActionContextParameter) SaveWithDetailDTO saveWithDetailDTO);


    /** * Commit boolean. * 這個方法需要保持幂等和防懸掛 * * @param actionContext the action context * @return the boolean */
    public boolean simpleSaveCommit(BusinessActionContext actionContext);

    /** * Rollback boolean. * 這個方法需要保持幂等和防懸掛 * * @param actionContext the action context * @return the boolean */
    public boolean simpleSaveRollback(BusinessActionContext actionContext);
}

2、實現類,因為我模擬的這個業務是插入,而二階段回滾的時候,補償肯定就是更具新增的id删除它,但是我試了一下,在第一階段的actionContext#map裏面增加參數,或者修改saveWithDetailDTO參數,都不行,在第二階段只能獲取到初始傳參的saveWithDetailDTO值,這也就是我前面提到的,如果有和我一樣的業務需求,可以考慮放到redis裏面拿id,等等。

@Slf4j
@Service
public class WorkOrderServiceImpl implements WorkOrderService {
    

    @DubboReference
    private WorkDeviceApi workDeviceApi;
    @DubboReference
    private WorkProblemApi workProblemApi;

    @Autowired
    private NoticeInfoMapper noticeInfoMapper;

    private static final String INSERT_ORDER_ID_KEY = "INSERT_ORDER_ID_KEY";
    private static final String INSERT_NOTICE_INFO_ID_KEY = "INSERT_NOTICE_INFO_ID_KEY";

    /** * * @param saveWithDetailDTO saveWithDetailDTO * @return */
// @Transactional 當然這個方法也可以加上spring 的 Transactional 注解
    @Override
    public boolean simpleSave(BusinessActionContext actionContext, SaveWithDetailDTO saveWithDetailDTO) {
    

        // 屬性 BusinessActionContext 不需要我們注入,seata會為我們注入的
        String actionName = actionContext.getActionName();
        String xid = actionContext.getXid();
        long branchId = actionContext.getBranchId();

        String title = RandomUtil.randomString(20);
        saveWithDetailDTO.setWorkOrderTitle(title);
        saveWithDetailDTO.setWorkOrderNumber("asd");

        // 1、調用自身服務
        // 1.1、插入工單信息
        WorkOrder workOrder = new WorkOrder();
        BeanUtils.copyProperties(saveWithDetailDTO, workOrder);
        this.baseMapper.insert(workOrder);
        saveWithDetailDTO.setId(workOrder.getId());
        // 即使你在這個理修改了BusinessActionContext存儲的數據,但是你在二階段(commit/rollback)是拿不到修改後的數據的
        // 只能拿到一開始初始化的數據,肯是因為在二階段的BusinessActionContext對象,是新實例,只有初始的數據
        // 沒有後面修改的數據
// Map<String, Object> actionContextMap = actionContext.getActionContext();
// actionContextMap.put(INSERT_ORDER_ID_KEY, workOrder.getId());


        // 1.2、插入消息通知錶
        NoticeInfo noticeInfo = new NoticeInfo();
        noticeInfo.setTitle("new work order 【" + title + "】has publish");
        noticeInfoMapper.insert(noticeInfo);
// actionContextMap.put(INSERT_NOTICE_INFO_ID_KEY, noticeInfo.getId());

        return true;
    }

    @Override
    public boolean simpleSaveCommit(BusinessActionContext actionContext) {
    
        // 這裏就可以獲取 當初在prepare 階段的加上注解 BusinessActionContextParameter 的值
        log.info("simpleSave Commit, params : {}", JSONUtil.toJsonStr(actionContext.getActionContext(simpleSave_BusinessActionContextParameter)));

        //todo 若一階段資源預留,這裏則要提交資源
        // 錶示是否成功
        return true;
    }

    @Override
    public boolean simpleSaveRollback(BusinessActionContext actionContext) {
    
        // 這裏就可以獲取 當初在prepare 階段的加上注解 BusinessActionContextParameter 的值
        JSONObject saveWithDetailDTOJSONObject = (JSONObject) actionContext.getActionContext(simpleSave_BusinessActionContextParameter);
        log.info("simpleSave Commit , params : {}", JSONUtil.toJsonStr(saveWithDetailDTOJSONObject));

        // 補償措施,如下

        // 1、解决幂等 工單錶id 為空,說明第一步都還未執行成功,無需補償該步驟
        // 這裏可以換成從redis中獲取,這樣 Integer orderId = (Integer) actionContext.getActionContext(INSERT_ORDER_ID_KEY);,是獲取不到第一節端存入的值的
        // 我這裏為了方便演示就寫成1了,因為的每次演示完後,都會truncate table
        Integer orderId = 1;
        if (orderId == null) {
    
            return true;
        } else {
    
            // 删除插入work_order錶的數據
            this.baseMapper.deleteById(orderId);
        }

        // 2、解决幂等 消息通知錶 id 為空,說明未插入,無需補償該步驟
        // 這裏可以換成從redis中獲取,這樣 Integer noticeInfoId = (Integer) actionContext.getActionContext(INSERT_NOTICE_INFO_ID_KEY);,是獲取不到第一節端存入的值的
        // 我這裏為了方便演示就寫成1了,因為的每次演示完後,都會truncate table
        Integer noticeInfoId = 1;
        if (noticeInfoId == null) {
    
            return true;
        } else {
    
            // 删除插入notice_Info錶的數據
            noticeInfoMapper.deleteById(noticeInfoId);
        }
        return true;
    }
}

2.1.3、device服務

1、注意我們要在接口上加上注解@LocalTCC,開啟tcc事務,並在第一階段的方法上加上注解@TwoPhaseBusinessAction,並賦值注解的值,錶明第二階段的commitrollback方法分別是什麼,以及三個方法的返回值得是boolean

@LocalTCC
public interface WorkProblemApi {
    

    String insertWithLink_BusinessActionContextParameter = "workProblemDTO";

    /** * 插入時,插入對應的工單問題錶 * * @param workProblemDTO * @return */
    @TwoPhaseBusinessAction(name = "DubboTccInsertWithLinkActionTwo", commitMethod = "insertWithLinkCommit", rollbackMethod = "insertWithLinkRollback")
    boolean insertWithLink(BusinessActionContext actionContext,
                           @BusinessActionContextParameter(paramName = insertWithLink_BusinessActionContextParameter) WorkProblemDTO workProblemDTO);


    /** * Commit boolean. * * @param actionContext the action context * @return the boolean */
    public boolean insertWithLinkCommit(BusinessActionContext actionContext);

    /** * Rollback boolean. * * @param actionContext the action context * @return the boolean */
    public boolean insertWithLinkRollback(BusinessActionContext actionContext);
}

2、實現類,因為我模擬的這個業務是插入,而二階段回滾的時候,補償肯定就是更具新增的id删除它,但是我試了一下,在第一階段的actionContext#map裏面增加參數,或者修改workProblemDTO參數,都不行,在第二階段只能獲取到初始傳參的workProblemDTO值,這也就是我前面提到的,如果有和我一樣的業務需求,可以考慮放到redis裏面拿id,等等。

@Slf4j
@DubboService
public class WorkProblemApiImpl implements WorkProblemApi {
    

    @Autowired
    private WorkProblemMapper workProblemMapper;

    @Autowired
    private WorkOrderProblemLinkMapper workOrderProblemLinkMapper;

    private static final String INSERT_PROBLEM_ID_KEY = "INSERT_PROBLEM_ID_KEY";
    private static final String INSERT_ORDER_PROBLEM_LINK_ID_KEY = "INSERT_ORDER_PROBLEM_LINK_ID_KEY";

    @Override
    public boolean insertWithLink(BusinessActionContext actionContext, WorkProblemDTO workProblemDTO) {
    
        WorkProblem workProblem = new WorkProblem();
        BeanUtils.copyProperties(workProblemDTO, workProblem);
        // 1、插入問題錶
        int insertProblem = workProblemMapper.insert(workProblem);

        // 2、插入工單問題關聯錶
        WorkOrderProblemLink workOrderProblemLink = new WorkOrderProblemLink();
        workOrderProblemLink.setOrderId(workProblemDTO.getOrderId());
        workOrderProblemLink.setProblemId(workProblem.getId());
        int insertOrderProblemLink = workOrderProblemLinkMapper.insert(workOrderProblemLink);
        // 模擬异常
        int i = 1 / 0;

        if (insertProblem > 0 && insertOrderProblemLink > 0) {
    
            return true;
        }
        throw new RuntimeException("插入异常");
    }

    @Override
    public boolean insertWithLinkCommit(BusinessActionContext actionContext) {
    
        // 這裏就可以獲取 當初在prepare 階段的加上注解 BusinessActionContextParameter 的值
        log.info("insertWithLink commit, params : {}", JSONUtil.toJsonStr(actionContext.getActionContext(insertWithLink_BusinessActionContextParameter)));
        
		//todo 若一階段資源預留,這裏則要提交資源
        // 錶示是否成功
        return true;
    }

    @Override
    public boolean insertWithLinkRollback(BusinessActionContext actionContext) {
    
        // 這裏就可以獲取 當初在prepare 階段的加上注解 BusinessActionContextParameter 的值
        JSONObject workProblemDTOJSONObject = (JSONObject) actionContext.getActionContext(insertWithLink_BusinessActionContextParameter);
        log.info("insertWithLink Rollback, params : {}", JSONUtil.toJsonStr(workProblemDTOJSONObject));

        // 補償措施,如下

        // 1、解决幂等 問題錶id 為空,說明第一步都還未執行成功,無需補償該步驟
        // 這裏可以換成從redis中獲取,這樣 (Integer) actionContext.getActionContext(INSERT_PROBLEM_ID_KEY);,是獲取不到第一節端存入的值的
        // 我這裏為了方便演示就寫成1了,因為的每次演示完後,都會truncate table
        Integer insertProblemId = 1;
        if (insertProblemId == null) {
    
            return true;
        } else {
    
            // 删除插入work_order錶的數據
            this.workProblemMapper.deleteById(insertProblemId);
        }

        // 2、解决幂等 工單問題關聯錶 id 為空,說明未插入,無需補償該步驟
        // 這裏可以換成從redis中獲取,這樣 (Integer) actionContext.getActionContext(INSERT_ORDER_PROBLEM_LINK_ID_KEY); 是獲取不到第一節端存入的值的
        // 我這裏為了方便演示就寫成1了,因為的每次演示完後,都會truncate table
        Integer insertOrderProblemLinkId = 1;
        if (insertOrderProblemLinkId == null) {
    
            return true;
        } else {
    
            // 删除插入work_order_problem_link錶的數據
            workOrderProblemLinkMapper.deleteById(insertOrderProblemLinkId);
        }
        return true;
    }
}

2.1.4、測試分析結果

1、我們在4個mapper都執行完成時,且异常還未還發生的地方打一個斷點,如下
在這裏插入圖片描述

2、4張業務錶中都插入了 對於的數據,此時因為我們使用的是tcc模式,rollback的事情需要我們自己去處理,所以undo_log錶中是沒有數據的,你也可以直接删除這個undo_log錶
在這裏插入圖片描述

3、seata 服務端3張錶,可以看到branch_table錶中的分支類型已經換成了TCC模式,一個兩個分支,分別是order服務的tcc,和device服務的tcc,錶中還有一個字段application-data就是你操作的數據。

在這裏插入圖片描述
4、放開斷點後,可以看到發生了异常,所以2個服務(4個mapper)都要回滾

5、order服務日志分析如下
在這裏插入圖片描述
在這裏插入圖片描述

6、device服務日志分析如下
在這裏插入圖片描述

在這裏插入圖片描述

7、查看數據庫,當然你也可以看下每個錶的自增id,是否已經從2開始了,如果從2開始了,就說明,之前有插入,不過後面回滾删除了。
在這裏插入圖片描述

2.2、如何控制异常

1、這部分內容來自於 seata-TCC模式

2、在 TCC 模型執行的過程中,還可能會出現各種异常,其中最為常見的有空回滾、幂等、懸掛等。下面我講下 Seata 是如何處理這三種异常的

2.2.1、如何處理空回滾

1、什麼是空回滾?

空回滾指的是在一個分布式事務中,在沒有調用參與方的 Try 方法的情况下,TM 驅動二階段回滾調用了參與方的 Cancel 方法。

2、那麼空回滾是如何產生的呢?

在全局事務開啟後,參與者 A 分支注册完成之後會執行參與者一階段 RPC 方法,如果此時參與者 A 所在的機器發生宕機,網絡异常,都會造成 RPC 調用失敗,即參與者 A 一階段方法未成功執行,但是此時全局事務已經開啟,Seata 必須要推進到終態,在全局事務回滾時會調用參與者 A 的 Cancel 方法,從而造成空回滾。

3、要想防止空回滾,那麼必須在 Cancel 方法中識別這是一個空回滾,Seata 是如何做的呢?

Seata 的做法是新增一個 TCC 事務控制錶,包含事務的 XID 和 BranchID 信息,在 Try 方法執行時插入一條記錄,錶示一階段執行了,執行 Cancel 方法時讀取這條記錄,如果記錄不存在,說明 Try 方法沒有執行。

2.2.2、如何處理幂等

1、幂等問題指的是 TC 重複進行二階段提交,因此 Confirm/Cancel 接口需要支持幂等處理,即不會產生資源重複提交或者重複釋放。

2、那麼幂等問題是如何產生的呢?

在參與者 A 執行完二階段之後,由於網絡抖動或者宕機問題,會造成 TC 收不到參與者 A 執行二階段的返回結果,TC 會重複發起調用,直到二階段執行結果成功。

3、Seata 是如何處理幂等問題的呢?

同樣的也是在 TCC 事務控制錶中增加一個記錄狀態的字段 status,該字段有 3 個值,分別為:

  • tried:1
  • committed:2
  • rollbacked:3

二階段 Confirm/Cancel 方法執行後,將狀態改為 committed 或 rollbacked 狀態。當重複調用二階段 Confirm/Cancel 方法時,判斷事務狀態即可解决幂等問題。

2.2.3、如何處理懸掛

1、懸掛指的是二階段 Cancel 方法比 一階段 Try 方法優先執行,由於允許空回滾的原因,在執行完二階段 Cancel 方法之後直接空回滾返回成功,此時全局事務已結束,但是由於 Try 方法隨後執行,這就會造成一階段 Try 方法預留的資源永遠無法提交和釋放了。

2、那麼懸掛是如何產生的呢?

在執行參與者 A 的一階段 Try 方法時,出現網路擁堵,由於 Seata 全局事務有超時限制,執行 Try 方法超時後,TM 决議全局回滾,回滾完成後如果此時 RPC 請求才到達參與者 A,執行 Try 方法進行資源預留,從而造成懸掛。

3、Seata 是怎麼處理懸掛的呢?

在 TCC 事務控制錶記錄狀態的字段 status 中增加一個狀態:

  • suspended:4

當執行二階段 Cancel 方法時,如果發現 TCC 事務控制錶有相關記錄,說明二階段 Cancel 方法優先一階段 Try 方法執行,因此插入一條 status=4 狀態的記錄,當一階段 Try 方法後面執行時,判斷 status=4 ,則說明有二階段 Cancel 已執行,並返回 false 以阻止一階段 Try 方法執行成功。

4、代碼中可以增加參數useTCCFence = true,開啟seata的放懸掛

@TwoPhaseBusinessAction(name = "beanName", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)

三、SAGA 模式

1、這個saga模式坑的地方是真多啊,主要是官方的文檔真是太亂了,但其實源碼下的測試用例還是不錯的,就是文檔太少了,為了找一個可以用於生產的情况,我真是東凑西凑啊。

2、第一點,官方的saga模式的文檔是一定要看的SEATA Saga 模式,看了之後就可以大致了解下了,裏面的狀態語言的參數的含義大家都是要知道的,不然後面寫不了。

3、然後官方代碼示例,建議剛入手的小夥伴,一定要先都過一遍,心裏有個底

  • 源碼下的測試用例,io.seata.saga.engine.StateMachineTests,列舉了幾乎所有的狀態機情况

在這裏插入圖片描述

在這裏插入圖片描述

  • 還有一個是官方的示例代碼,項目地址是 seata-samples,找到你需要的項目情况的示例,當前的saga模式如下
    在這裏插入圖片描述

3.1、代碼模擬

1、根據上面的官方文檔和示例項目代碼,我們知道,saga目前提供了基於狀態機的方式,而狀態機的語言官方也給出了一個可視化的界面 狀態機設計器演示地址:http://seata.io/saga_designer/index.html

但是這個在線工具,似乎不支持一些老版本
在這裏插入圖片描述

這個沒有上面的詳細,應該是第一版本 在這裏插入圖片描述

在這裏插入圖片描述

3.1.1 創建數據庫

1、saga模式需要在服務發起方的數據庫增加一些錶,當然saga有提供基於內存數據庫(H2)的模式,但是官方不建議你那麼做。
在這裏插入圖片描述
2、具體如下
在這裏插入圖片描述
3、執行sql脚本,因為我後續的演示是從order服務發起,用的數據庫是gxm-300,所以我這個sql脚本就執行在哪裏,如下圖新增了三張錶。

在這裏插入圖片描述

3.1.2 業務代碼

1、很前面一樣,我們抽取一個專門處理複雜業務的service類出來,裏面分別調用和device服務

  • order服務(使用gxm-300數據庫的 work_order錶和notice_info錶)
  • device服務(使用gxm-301數據庫的 work_order_problem_link錶和work_problem錶)

3.1.2.1 order服務

1、WorkOrderService 接口類,一個是業務方法,另一個就是那個業務失敗的補償方法。
在這裏插入圖片描述

2、WorkOrderServiceImpl 實現類,一個是業務方法,另一個就是那個業務失敗的補償方法。

package cn.gxm.order.service.impl;

import cn.gxm.order.dto.method.service.savewithdetail.SaveWithDetailDTO;
import cn.gxm.order.mapper.NoticeInfoMapper;
import cn.gxm.order.mapper.WorkOrderMapper;
import cn.gxm.order.pojo.NoticeInfo;
import cn.gxm.order.pojo.WorkOrder;
import cn.gxm.order.service.WorkOrderService;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/** * @author GXM * @version 1.0.0 * @Description TODO * @createTime 2022年04月14日 */
@Slf4j
@Service
public class WorkOrderServiceImpl extends ServiceImpl<WorkOrderMapper, WorkOrder> implements WorkOrderService {
    

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private NoticeInfoMapper noticeInfoMapper;

    /** * 測試seata * * @param saveWithDetailDTO saveWithDetailDTO * @return */
    @Override
    public SaveWithDetailDTO simpleSave(String businessKey, SaveWithDetailDTO saveWithDetailDTO) {
    

        // 可以看到我們的 workProblemApi 在不在spring 裏面
// System.out.println(applicationContext.getBeanDefinitionCount());
// for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
    
// System.out.println(beanDefinitionName);
// }

        String title = RandomUtil.randomString(20);
        saveWithDetailDTO.setWorkOrderTitle(title);
        saveWithDetailDTO.setWorkOrderNumber("asd");

        // 1、調用自身服務
        // 1.1、插入工單信息
        WorkOrder workOrder = new WorkOrder();
        BeanUtils.copyProperties(saveWithDetailDTO, workOrder);
        workOrder.setBusinessKey(businessKey);
        this.baseMapper.insert(workOrder);
        saveWithDetailDTO.setId(workOrder.getId());

        // 1.2、插入消息通知錶
        NoticeInfo noticeInfo = new NoticeInfo();
        noticeInfo.setTitle("new work order 【" + title + "】has publish");
        noticeInfo.setBusinessKey(businessKey);
        noticeInfoMapper.insert(noticeInfo);
        saveWithDetailDTO.setNoticeInfoId(noticeInfo.getId());

        return saveWithDetailDTO;
    }


    @Override
    public boolean compensateCreateOrder(String businessKey) {
    
        log.info("compensateCreateOrder business key : {}", businessKey);
        if (StrUtil.isNotBlank(businessKey)) {
    
            // 1、根據 business key 來進行操作 補償 因為我這裏的業務是插入,所以,我直接根據business key 删除相關數據即可

            // 1.1、删除 work_order 錶數據
            LambdaQueryWrapper<WorkOrder> workOrderQueryWrapper = new LambdaQueryWrapper<>();
            workOrderQueryWrapper.eq(WorkOrder::getBusinessKey, businessKey);
            this.baseMapper.delete(workOrderQueryWrapper);

            // 1.2、删除 notice_info 錶數據
            LambdaQueryWrapper<NoticeInfo> noticeInfoQueryWrapper = new LambdaQueryWrapper<>();
            noticeInfoQueryWrapper.eq(NoticeInfo::getBusinessKey, businessKey);
            noticeInfoMapper.delete(noticeInfoQueryWrapper);
        }

        return true;
    }
}

3.1.2.1 device服務

1、WorkProblemApi 接口類,一個是業務方法,另一個就是那個業務失敗的補償方法。
在這裏插入圖片描述
2、WorkProblemApiImpl實現類,一個是業務方法,另一個就是那個業務失敗的補償方法。

package cn.gxm.device.client;

import cn.gxm.device.api.WorkProblemApi;
import cn.gxm.device.dto.common.WorkProblemDTO;
import cn.gxm.device.mapper.WorkOrderProblemLinkMapper;
import cn.gxm.device.mapper.WorkProblemMapper;
import cn.gxm.device.pojo.WorkOrderProblemLink;
import cn.gxm.device.pojo.WorkProblem;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;

/** * @author GXM * @version 1.0.0 * @Description TODO * @createTime 2022年05月19日 */
@Slf4j
@DubboService
public class WorkProblemApiImpl implements WorkProblemApi {
    

    @Autowired
    private WorkProblemMapper workProblemMapper;

    @Autowired
    private WorkOrderProblemLinkMapper workOrderProblemLinkMapper;


    @Override
    public WorkProblemDTO insertWithLink(String businessKey, WorkProblemDTO workProblemDTO) {
    
        WorkProblem workProblem = new WorkProblem();
        BeanUtils.copyProperties(workProblemDTO, workProblem);
        workProblem.setBusinessKey(businessKey);
        // 1、插入問題錶
        workProblemMapper.insert(workProblem);
        workProblemDTO.setId(workProblem.getId());

        // 2、插入工單問題關聯錶
        WorkOrderProblemLink workOrderProblemLink = new WorkOrderProblemLink();
        workOrderProblemLink.setOrderId(workProblemDTO.getOrderId());
        workOrderProblemLink.setProblemId(workProblem.getId());
        workOrderProblemLink.setBusinessKey(businessKey);
        workOrderProblemLinkMapper.insert(workOrderProblemLink);
        workProblemDTO.setOrderProblemLinkId(workOrderProblemLink.getId());

        if (workProblemDTO.getProblem().equals("exception")) {
    
            int i = 1 / 0;
        }
        return workProblemDTO;
    }

    /** * 業務補償 * * @param businessKey seata業務key * @return */
    @Override
    public boolean compensateInsertWithLink(String businessKey) {
    
        log.info("compensateInsertWithLink business key : {}", businessKey);
        if (StrUtil.isNotBlank(businessKey)) {
    
            // 1、根據 business key 來進行操作 補償 因為我這裏的業務是插入,所以,我直接根據business key 删除相關數據即可

            // 1.1、删除 work_problem 錶數據
            LambdaQueryWrapper<WorkProblem> workProblemQueryWrapper = new LambdaQueryWrapper<>();
            workProblemQueryWrapper.eq(WorkProblem::getBusinessKey, businessKey);
            workProblemMapper.delete(workProblemQueryWrapper);

            // 1.2、删除 notice_info 錶數據
            LambdaQueryWrapper<WorkOrderProblemLink> workOrderProblemLinkQueryWrapper = new LambdaQueryWrapper<>();
            workOrderProblemLinkQueryWrapper.eq(WorkOrderProblemLink::getBusinessKey, businessKey);
            workOrderProblemLinkMapper.delete(workOrderProblemLinkQueryWrapper);
        }
        return true;
    }
}

3.1.2.3 綜合複雜業務類

1、我這邊把這個類型直接寫到了order服務下
在這裏插入圖片描述

2、實現類

package cn.gxm.order.service.impl;

import cn.gxm.common.resp.R;
import cn.gxm.order.dto.method.service.savewithdetail.SaveWithDetailDTO;
import cn.gxm.order.service.BusinessService;
import io.seata.core.context.RootContext;
import io.seata.saga.engine.StateMachineEngine;
import io.seata.saga.statelang.domain.StateMachineInstance;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

/** * @author GXM * @version 1.0.0 * @Description TODO * @createTime 2022年06月10日 */
@Service
@Slf4j
public class BusinessServiceImpl implements BusinessService {
    


    @Autowired
    private StateMachineEngine stateMachineEngine;

    /** * 業務邏輯為 * // 1、order 服務 * workOrderService.simpleSave(saveWithDetailDTO); * // 2、遠程調用 device 服務 * // 2.1、插入問題錶 和問題關聯錶 * WorkProblemDTO workProblemDTO = new WorkProblemDTO(); * BeanUtils.copyProperties(saveWithDetailDTO.getSoftwareNotSolveProblemList().get(0), workProblemDTO); * workProblemDTO.setOrderId(saveWithDetailDTO.getId()); * workProblemApi.insertWithLink(workProblemDTO); * * @param saveWithDetailDTO * @return */
    @Override
    public R saveWithDetailInStatemachineEngine(SaveWithDetailDTO saveWithDetailDTO) {
    
        log.info("create order begin ... xid: " + RootContext.getXID());

        String businessKey = String.valueOf(System.currentTimeMillis());

        // 1、下面這個狀態機描述的就是上面的過程
        Map<String, Object> paramMap = new HashMap<>(1);
        // 1.1、這個 `saveWithDetailDTOKey`,可以在狀態機json文件中通過 $.[saveWithDetailDTOKey] 獲取,而且加上`.`可以錶示具體的數據
        paramMap.put("saveWithDetailDTOKey", saveWithDetailDTO);
        paramMap.put("businessKey", businessKey);

        String stateMachineName = "createOrderAndProblemStateMachine";
        // 1.2、執行狀態機json文件,具體業務流程都在json文件中。
// StateMachineInstance instance = stateMachineEngine.start(stateMachineName, null, paramMap);
        StateMachineInstance instance = stateMachineEngine.startWithBusinessKey(stateMachineName, null, businessKey, paramMap);
        log.info("最總執行結果: {}; xid : {}; businessKey: {}; compensationStatus {}",
                instance.getStatus(), instance.getId(), instance.getBusinessKey(), instance.getCompensationStatus());

        return R.ok();
    }
}

3.1.3 項目配置saga 模式

1、首先我們需要寫一個我們的業務的狀態語言文件,來錶示你的業務情况,以及回滾補償的情况,你可以使用前面提到過的那個官方提供的在線工具

一些語法我就不再細說了,官方文檔拉到底部,就是說這些語義的,不明白可以去看下

在這裏插入圖片描述

{
    
  "Name": "createOrderAndProblemStateMachine",  # 狀態機的名稱,後續使用的時候要根據這個唯一來找
  "Comment": "創建工單狀態機", # 簡介
  "StartState": "CreateOrder",  # 初始狀態
  "Version": "0.0.1", # 當前版本
  "States": {
      # 狀態列錶
    "CreateOrder": {
     # 名為CreateOrder的狀態列錶
      "Type": "ServiceTask", # 類型
      "ServiceName": "workOrderServiceImpl", # 對應的服務bean名稱,saga會到spring的bean容器中找這個名稱的bean。
      "ServiceMethod": "simpleSave",    # workOrderServiceImpl的名為simpleSave的方法
      "Next": "ChoiceState",  # 下一個狀態
      "CompensateState": "CompensateCreateOrder",  # 當前服務的補償狀態的名稱(下面有定義)
      "ParameterTypes": [  # workOrderServiceImpl#simpleSave 的方法參數類型(可以不寫,但是如果有泛型,就要寫,官方有說明)
        "java.lang.String",
        "cn.gxm.order.dto.method.service.savewithdetail.SaveWithDetailDTO"
      ],
      "Input": [    # workOrderServiceImpl#simpleSave 的方法的參數值
        "$.[businessKey]",
        "$.[saveWithDetailDTOKey]"
      ],
      "Output": {
     # workOrderServiceImpl#simpleSave 的方法的返回值,存儲在狀態機上下文中,key是 simpleSaveResult,值是該方法的整個返回結果
        "simpleSaveResult": "$.#root"
      },
      "Status": {
      # 當前CreateOrder的的狀態 服務執行狀態映射,框架定義了三個狀態,SU 成功、FA 失敗、UN 未知, 我們需要把服務執行的狀態映射成這三個狀態,幫助框架判斷整個事務的一致性,是一個map結構,key是條件錶達式,一般是取服務的返回值或拋出的异常進行判斷,默認是SpringEL錶達式判斷服務返回參數,帶$Exception{
    開頭錶示判斷异常類型。value是當這個條件錶達式成立時則將服務執行狀態映射成這個值
      # 這裏要說的一點就是這個异常的判斷得放到前面,不然如果你把根據返回值的判斷放到前面,一旦發生异常,那麼方法是沒有返回值的,那這個#root.id就是錯誤的語法,因為#root是null,當然最總的狀態還是"UN"
        "$Exception{java.lang.Throwable}": "UN",
        "#root.id != null && #root.noticeInfoId != null": "SU",
        "#root.id == null || #root.noticeInfoId == null": "FA"
      }
    },
    "ChoiceState": {
    
      "Type": "Choice",
      "Choices": [
        {
    
           # 只有CreateOrder階段創建成功了(work_order有id了,並且notice_info也有id,說明插入成功了),才走下一步
          "Expression": "[simpleSaveResult].id != null && [simpleSaveResult].noticeInfoId != null",
          "Next": "CreateProblem"
        }
      ],
      "Default": "Fail" # 否則默認失敗(失敗狀態下面有定義)
    },
    "CreateProblem": {
    
      "Type": "ServiceTask",
      "ServiceName": "workProblemApi",
      "ServiceMethod": "insertWithLink",
      "CompensateState": "CompensateCreateProblem",
      "Input": [
        "$.[businessKey]",
        {
    
          "problem": "$.[saveWithDetailDTOKey].softwareNotSolveProblemList[0].problem",
          "type": "$.[saveWithDetailDTOKey].softwareNotSolveProblemList[0].type",
          "orderId": "$.[simpleSaveResult].id"
        }
      ],
      "Output": {
    
        "insertWithLinkResult": "$.#root"
      },
      "Status": {
    
        "$Exception{java.lang.Throwable}": "UN",
        "#root.id != null && #root.orderProblemLinkId != null": "SU",
        "#root.id == null || #root.orderProblemLinkId == null": "FA"
      },
      "Catch": [
        {
    
          "Exceptions": [
            "java.lang.Throwable"
          ],
          "Next": "CompensationTrigger"
        }
      ],
      "Next": "Succeed"
    },
    "CompensateCreateOrder": {
      # CreateOrder的補償措施
      "Type": "ServiceTask",
      "ServiceName": "workOrderServiceImpl", # 需要 workOrderServiceImpl 的bean
      "ServiceMethod": "compensateCreateOrder", # 調用 workOrderServiceImpl#compensateCreateOrder方法裏面
      "Input": [
        "$.[businessKey]"
      ]
    },
    "CompensateCreateProblem": {
    
      "Type": "ServiceTask",
      "ServiceName": "workProblemApi",
      "ServiceMethod": "compensateInsertWithLink",
      "Input": [
        "$.[businessKey]"
      ]
    },
    "CompensationTrigger": {
    
      "Type": "CompensationTrigger",
      "Next": "Fail"
    },
    "Succeed": {
    
      "Type": "Succeed"
    },
    "Fail": {
    
      "Type": "Fail",
      "ErrorCode": "CREATE_FAILED",
      "Message": "create order failed"
    }
  }
}

2、配置saga的狀態機的配置信息,比如你的狀態機json文件叫什麼,在哪裏,並注入到spring的容器中,官方的示例,大家看下都能看出來是xml,我這裏就改為springboot的配置方式注入就行,大家可以隨意選擇一個方式
在這裏插入圖片描述
3、我這裏改為springboot的@Configuration注入方式,裏面內容就是對應上面的xml文件內容,

其中有一個地方需要注意,狀態機在執行的時候,會去spring的bean中對應的bean,我們使用dubbo的方式注入的時候,並不會在spring的容器內部,所以會出現找不到對應的bean,但其實我們使用@DubboReference是可以獲取的,所以,這裏我們手動注入一下。
在這裏插入圖片描述

在這裏插入圖片描述

package cn.gxm.order.config;

import cn.gxm.device.api.WorkProblemApi;
import cn.gxm.order.service.impl.WorkOrderServiceImpl;
import com.zaxxer.hikari.HikariDataSource;
import io.seata.saga.engine.StateMachineEngine;
import io.seata.saga.engine.config.DbStateMachineConfig;
import io.seata.saga.engine.impl.ProcessCtrlStateMachineEngine;
import io.seata.saga.rm.StateMachineEngineHolder;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import javax.sql.DataSource;
import java.io.File;

/** * @author GXM * @version 1.0.0 * @Description seata saga模式配置信息 * @createTime 2022年06月13日 */
@Configuration
public class SeataSagaConfig {
    


    /** * bean 默認是方法名,這裏不寫也可以,但是為了防明確語義 還是寫一下的好 * * @return */
    @Bean(name = "seataSagaDataSource")
    public DataSource seataSagaDataSource() {
    
        HikariDataSource dataSource = new HikariDataSource();
        // 這個數據庫地址是 seata_state_inst 、seata_state_machine_def、seata_state_machine_inst 三張錶的地址,一般是在業務發起方的數據庫中
        dataSource.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/gxm-300?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=UTF-8");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        return dataSource;
    }

    @Bean(name = "dbStateMachineConfig")
    public DbStateMachineConfig dbStateMachineConfig(@Qualifier("seataSagaDataSource") DataSource seataSagaDataSource) {
    
        DbStateMachineConfig dbStateMachineConfig = new DbStateMachineConfig();
        dbStateMachineConfig.setDataSource(seataSagaDataSource);

        ClassPathResource resource = new ClassPathResource("statelang" + File.separator + "create_order_and_problem.json");
        dbStateMachineConfig.setResources(new Resource[]{
    resource});
        dbStateMachineConfig.setEnableAsync(true);
        // 執行線程(這個官方文檔怪怪的,類型都不匹配......) 事件驅動執行時使用的線程池, 如果所有狀態機都同步執行且不存在循環任務可以不需要
// dbStateMachineConfig.setThreadPoolExecutor();
        dbStateMachineConfig.setApplicationId("test_saga");
        dbStateMachineConfig.setTxServiceGroup("my_test_tx_group");

        return dbStateMachineConfig;
    }

    /** * saga 狀態機 實例 */
    @Bean(name = "stateMachineEngine")
    public StateMachineEngine stateMachineEngine(@Qualifier("dbStateMachineConfig") DbStateMachineConfig dbStateMachineConfig) {
    
        ProcessCtrlStateMachineEngine processCtrlStateMachineEngine = new ProcessCtrlStateMachineEngine();
        processCtrlStateMachineEngine.setStateMachineConfig(dbStateMachineConfig);
        return processCtrlStateMachineEngine;
    }


    /** * Seata Server進行事務恢複時需要通過這個Holder拿到stateMachineEngine實例 * * @param stateMachineEngine * @return */
    @Bean
    public StateMachineEngineHolder stateMachineEngineHolder(@Qualifier("stateMachineEngine") StateMachineEngine stateMachineEngine) {
    
        StateMachineEngineHolder stateMachineEngineHolder = new StateMachineEngineHolder();
        stateMachineEngineHolder.setStateMachineEngine(stateMachineEngine);
        return stateMachineEngineHolder;
    }


    @DubboReference
    private WorkProblemApi workProblemApi;

    /** * 因為2.x版本的 dubbo 使用 注解 @DubboReference 時,不會注入到spring 中(@DubboReference 並不是 Spring定義的 Bean,所以不會生成 BeanDefinition ,也就是不會主動 createBean ,只能在屬性注入的時候觸發), * 而saga的狀態機在讀取的 * 時候要從spring 中獲取其他服務的bean,所以這裏手動注入一下 * 具體分析可以看 https://heapdump.cn/article/3610812 * @return */
    @Bean(name = "workProblemApi")
    public WorkProblemApi workProblemApi() {
    
        return workProblemApi;
    }
}

3.1.4、說明(重要)

1、其中有一個問題,需要說明一下,就是參數businessKey,我們每次開始一個一個業務都獲取當前時時間戳作為businessKey,傳入到業務邏輯中去,這是為了,後續補償的時候,知道怎麼補償,比如說,我們當前這個業務,如果失敗,我們肯定要找到對應4張錶的4條數據,然後去删除,我們只要把生成的主鍵id,放到狀態機的全局對象中進行流轉,即可,但是有一種情况就是,一旦某一個狀態發生了异常,那麼在狀態機中是沒有返回數據的,那麼就無法將id傳入下一步,那後面補償業務怎麼辦呢。所以,這裏有兩種方式

2、第一種,本文做的這種,把業務key傳入,而且對應的四張錶,都需要一個businessKey字段,對應業務修改的時候,把businessKey填充上去,那麼進行補償的時候,直接根據業務key來操作即可。

3、第二種,還是傳入業務key,但是不在對應錶中怎加業務key字段,而是保存到redis這種第三方中,比如當前業務,在插入數據庫的時候,存入一個 hmap,key就是業務key,filed就是對應的錶名,值就是生成的id,那麼補償的時候,根據業務key從redis中取即可。

3.2、情况測試

3.2.1、正常情况

3.2.1.1、狀態機對象分析

1、我們先測試沒有异常情况的案例,在業務service中,打上斷點,查看執行的數據結果
在這裏插入圖片描述
2、初步結果如下:
在這裏插入圖片描述
3、傳入參數
在這裏插入圖片描述
4、結果參數
在這裏插入圖片描述

5、最總執行結果
在這裏插入圖片描述
6、狀態機對象
在這裏插入圖片描述
7、 第一個狀態機
在這裏插入圖片描述

8、 第二個狀態機
在這裏插入圖片描述

3.2.1.2、控制臺日志

1、先執行CreateOrder的狀態機,也就是插入order服務的兩張錶
在這裏插入圖片描述

2、再執行CreateProblem的狀態機,也就是插入device服務的兩張錶

在這裏插入圖片描述

3、最總結果如下,我們關注instance.getStatus()instance.getCompensationStatus()有沒有問題即可。

在這裏插入圖片描述

3.2.1.3、數據庫數據(後續有時間把這部分錶的含義補上)

1、order服務下的gxm-300數據庫情况,當然業務錶notice_infowork_order數據是肯定在的,我就不放圖了
在這裏插入圖片描述

2、device服務下的gxm-301數據庫情况,當然業務錶work_order_problem_linkwork_problem數據是肯定在的,我就不放圖了
在這裏插入圖片描述
3、seata服務端的三張錶數據

在這裏插入圖片描述

3.2.2、异常情况

1、測試該情况之前,把錶數據清空一下

truncate table `gxm-300`.notice_info;
truncate table `gxm-300`.undo_log;
truncate table `gxm-300`.work_order;
truncate table `gxm-300`.seata_state_inst;
truncate table `gxm-300`.seata_state_machine_def;
truncate table `gxm-300`.seata_state_machine_inst;



truncate table `gxm-301`.undo_log;
truncate table `gxm-301`.work_order_problem_link;
truncate table `gxm-301`.work_problem;


truncate table `seata`.branch_table;
truncate table `seata`.global_table;
truncate table `seata`.lock_table;

3.2.2.1、狀態機對象分析

1、我們在device服務端拋出個异常
在這裏插入圖片描述
2、接著還在之前的log比特置打上斷點
在這裏插入圖片描述
3、狀態機對象
在這裏插入圖片描述
4、這裏說一下這個狀態,這個狀態就是正常的(在有補償的情况下),可以看官方的說明
在這裏插入圖片描述

在這裏插入圖片描述

5、狀態機列錶為4個。
在這裏插入圖片描述

3.2.2.2、控制臺日志

1、先執行CreateOrder的狀態機,也就是插入order服務的兩張錶,沒有問題,因為此時業務都還是正常的。
在這裏插入圖片描述

2、再執行CreateProblem的狀態機,也就是插入device服務的兩張錶,然後報錯java.lang.ArithmeticException: / by zero
在這裏插入圖片描述
3、order服務收到device的錯誤信息
在這裏插入圖片描述
4、開始走補償狀態
在這裏插入圖片描述
5、先補償device服務,因為它最後執行 (這張圖應該在第4張的中間,執行完成後,你可以看到 State[CompensateCreateProblem] finish with status[SU])

在這裏插入圖片描述
6、再補償order服務

在這裏插入圖片描述
7、order補償也成功,最總結果

最總執行結果: UN; xid : 192.168.172.232:8091:279996470823649280; businessKey: 1655191560707; compensationStatus SU

在這裏插入圖片描述

3.2.2.3、數據庫數據(後續有時間把這部分錶的含義補上)

1、order服務下的gxm-300數據庫情况,當然業務錶notice_infowork_order數據是肯定不在的,因為回滾了,我就不放圖了

在這裏插入圖片描述
2、device服務下的gxm-301數據庫情况,當然業務錶work_order_problem_linkwork_problem數據是肯定不在的,因為回滾了,我就不放圖了
在這裏插入圖片描述

3、seata服務端的三張錶數據

在這裏插入圖片描述

3.2.3、補充說明

1、根據我們前面寫的狀態語言json文件知道,補償觸發點CompensationTrigger,是在CreateProblem的時候觸發的
在這裏插入圖片描述
2、那對於開始的狀態CreateOrder來說,它內部也有2個本地的mapper,而且它沒有設置補償觸發點,一旦直接在CreateOrder失敗怎麼辦呢,所以有兩種方式,

  • 第一種方式,CreateOrder階段失敗,也直接觸發補償點,這樣也直接執行CompensateCreateOrder而已,因為按照倒敘的方式補償,它就是第一個。
  • 第二種方式,不設置它觸發補償點,直接使用spring的事務回滾它就行,因為它是自己本地項目的的2個mapper。
    在這裏插入圖片描述
    3、這裏錶示一下第二種方式直接設置spring 事務回滾,如下
    在這裏插入圖片描述

3.3、其他問題

1、注意一旦中間狀態發生了异常,那麼這個狀態的結果你就很難拿到了
在這裏插入圖片描述
3、根據第2點,同理可得,我們在設置ServiceTask的狀態時,也是需要把异常判斷放在第一比特
在這裏插入圖片描述

四、XA 模式

4.1、使用說明

1、其實XAAT差不多,我的意思是代碼差不多,所以改動的地方不多,主要的一點是你使用的數據庫支持XA,比如MySQL就是可以的主要點就是開啟模式,默認就是AT模式(當然這個參數seata.data-source-proxy-mode是1.4.0開始提供的,之前的版本都只能通過代碼修改數據源代理來切換,下面有說)
在這裏插入圖片描述

2、第一我們需要修改代理數據源,如果你使用的是seata-starer,並且版本seata的版本 ≥1.4.0 可以直接使用注解的方式來替換,如下圖,

之前在AT模式在,不配置,是因為 seata-starer依賴,其內部內置GlobalTransactionScanner自動初始化功能,默認是AT模式,所以不用配置

在這裏插入圖片描述

3、但是如果你的版本沒有 ≥ 1.4.0,那麼你就只能使用代碼的方式去切換了,當然你可以直接選擇更新(seata更新,或者單獨更新,後面有說)

@Bean("dataSource")
    public DataSource dataSource(DruidDataSource druidDataSource) {
    
        // DataSourceProxy for AT mode
        // return new DataSourceProxy(druidDataSource);

        // DataSourceProxyXA for XA mode
        return new DataSourceProxyXA(druidDataSource);
    }

4、因為XA模式用不到undo_log錶,所以我們可以直接删除,最後gxm-300gxm-301如下
在這裏插入圖片描述

5、因為我這裏使用的是 spring-cloud-starter-alibaba-seata依賴,裏面的seata版本還是1.3.0版本,使用不了那個注解直接切換ATXA模式,如果我要是使用代碼改的話,還得從數據源到mapper,全部改一遍,實在有些麻煩,所以,我們可以手動提昇seata的版本,當然官網也是有建議的,可以用下面這種方式提示版本,所以我這裏就order服務和device服務的seata手動提昇到1.4.0版本
在這裏插入圖片描述
在這裏插入圖片描述

4.2、代碼修改

1、在device服務和order服務增加數據源代理配置(使用注解或者代碼,看你的版本或者你想用那個)
在這裏插入圖片描述

2、其他就和AT模式沒有區別了
在這裏插入圖片描述

3、如果項目沒有性能的要求我建議使用XA模式,因為,它是强一致性,而AT模式是最總一致性。解釋的話,看第五節,如何選擇四種模式。

4.3、正常測試(參考AT模式)

省略

4.4、异常測試(參考AT模式)

省略

4.5、測試seata回滾時,鏡像數據被其他事務修改後,無法回滾成功的情况(參考AT模式)

1、我們還是和AT模式一樣,增加一個接口,修改插入的數據

在這裏插入圖片描述

2、並在device服務休眠

在這裏插入圖片描述

3、記得修改全局事務時間和遠程調用組件的超時時間偶,AT模式有,這裏就不再多說了

4、在device服務休眠時間,我們調用改動接口,你會發現一直在阻塞,等到插入接口結束了,它也返回了,而且看控制臺的數據,發現沒有修改到數據,但是看日志插入語句不是先執行的嗎。這就是和AT模式的不同之處了。XA 如下

1、因為XA第一階段不會提交數據,會鎖住了那個資源到第二階段(你在它睡眠期間到數據庫看,是看不到那個插入的數據的),我們在第一階段執行完成後,調用修改接口,是 找不到 那個數據的。
在這裏插入圖片描述2、而AT模式是第一階段直接提交的,所以你能找到那個數據,後續失敗回滾是根據undo_log鏡像數據來進行回滾的,所以說AT模式是最總一致性,而XA模式是强一致性的。

在這裏插入圖片描述

五、如何選擇四種模式(强烈建議看下)

1、四種模式的優缺點和需要我們處理的地方,這篇文章都說了 分布式事務——Seata、XA、TCC、AT、SAGA模式

六、遇到的問題

6.1、Cannot construct instance of java.time.LocalDateTime

1、這個問題很多人都遇到過了,github的issues上面也提出了,主要原因是seata在回滾的時候,用到undo_log的鏡像數據,鏡像數據默認是fastjson序列化的,然後如果你的業務錶有時間字段,並且是datetime類型,那麼seata在回滾這類數據的時候,會受到影響。比如,我現在的業務錶notice_info就有這個時間字段,一旦涉及到這個業務的回滾,要去undo_log的錶中找之前這個錶的前後鏡像的數據,在反序列化時就會失敗。

在這裏插入圖片描述
2、這個是那個鏡像的內容,可以看到裏面確實有這個時間字段。
在這裏插入圖片描述
3、出現這個問題時,會在全局事務發起方,也就是使用了@GlobalTransactional注解的服務中無限的報錯,一直不停歇的報錯,你可以看到下圖,我都把那個服務關掉了,不然一直刷新那個錯誤。
在這裏插入圖片描述

4、解决方案,最後我采用的是降低MySQL版本到8.0.20

6.2、io.seata.core.exception.RmTransactionException: Response[ TransactionException[branch register request failed. xid=xx, msg=Data truncation: Data too

1、問題截圖如下
在這裏插入圖片描述
2、但是根據上面的日志你看不出來什麼,只是說數據大,上網搜索後,會發現說這個是因為lock_table錶在插入數據時,字段太長了。

3、所以具體是那個錶的那個字段有問題,不要根據網上的亂改,要看服務端日志,因為客戶端沒有說那個錶的那個字段,seata服務端日志如下,但是好像也沒有說那張錶,只是說PK字段,所以,你如果了解一些seata運行流程的化,就知道這是lock_table錶的pk字段
在這裏插入圖片描述
4、所以,我們修改一下lock_table錶的pk字段長度即可。
在這裏插入圖片描述

6.3、saga狀態機找不到dubbo的bean

1、我們通過 @BubboReference 是可以的,但是狀態機執行的時候,找不到
在這裏插入圖片描述
2、原因是因為2.x版本的 dubbo 使用 注解 @DubboReference 時,不會注入到spring 中(@DubboReference 並不是 Spring定義的 Bean,所以不會生成 BeanDefinition ,也就是不會主動 createBean ,只能在屬性注入的時候觸發),而saga的狀態機在讀取的 時候要從spring 中獲取其他服務的bean,所以這裏手動注入一下 具體分析可以看 https://heapdump.cn/article/3610812

3、解决方法就是我們提前手動注入到spring的bean容器中。
在這裏插入圖片描述

6.4、XA模式下出現 java.lang.NoSuchMethodException: com.mysql.cj.conf.PropertySet.getBooleanReadableProperty(java.lang.String)

4、這個原因是因為seata默認使用的是DruidDataSource數據庫連接池,而DruidDataSource裏面的 util 包中的MySqlUtils 類中的createXAConnection 方法,會使用MySQL驅動的getBooleanReadableProperty方法,但是高版本的MySQL驅動中這個方法沒有了,所以報錯,我這裏直接降低MySQL驅動版本即可,將 mysql 驅動包版本切換為8.0.11,在該版本中,getBooleanReadableProperty(String)方法是還存在的。

這個問題,github上面也提出來了,

在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述

原网站

版权声明
本文为[鐺鐺響]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/188/202207070843570417.html