Seata 全局鎖等待超時 問題排查

来源:https://www.cnblogs.com/cjsblog/archive/2023/03/28/17266281.html
-Advertisement-
Play Games

生產環境,一個簡單的事務方法,提交失敗,報 Global lock wait timeout 偽代碼如下: @GlobalTransactional(rollbackFor = Exception.class,timeoutMills = 30000,lockRetryInternal=3000,l ...


生產環境,一個簡單的事務方法,提交失敗,報 Global lock wait timeout

偽代碼如下:

@GlobalTransactional(rollbackFor = Exception.class,timeoutMills = 30000,lockRetryInternal=3000,lockRetryTimes=10)
@Override
public Boolean cancel(Long id, Long userId, Long companyId) {
    // 保存業務數據
    ...
    // 啟動工作流
    wkflAppServiceProvider.startProcess(....);
    ...
}

 異常如下:

org.springframework.dao.QueryTimeoutException: JDBC commit; Global lock wait timeout; nested exception is io.seata.rm.datasource.exec.LockWaitTimeoutException: Global lock wait timeout
                                                                                             
Caused by: io.seata.rm.datasource.exec.LockWaitTimeoutException: Global lock wait timeout
        at io.seata.rm.datasource.exec.LockRetryController.sleep(LockRetryController.java:63)
        at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.doRetryOnLockConflict(ConnectionProxy.java:346)
        at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.execute(ConnectionProxy.java:335)
        at io.seata.rm.datasource.ConnectionProxy.commit(ConnectionProxy.java:187)
        at org.springframework.jdbc.datasource.DataSourceTransactionManager.doCommit(DataSourceTransactionManager.java:333)
        ... 57 more
Caused by: io.seata.rm.datasource.exec.LockConflictException: get global lock fail, xid:10.222.248.60:8091:2900686326154883760, lockKeys:wkfl_app_auth:12326192,12326193;act_ge_bytearray:6515890,6515891;act_re_procdef:rediscountClickSubmitCancel_UserTask_0yze6zf_5:1:6515892;act_re_deployment:6515889
        at io.seata.rm.datasource.ConnectionProxy.recognizeLockKeyConflictException(ConnectionProxy.java:159)
        at io.seata.rm.datasource.ConnectionProxy.processGlobalTransactionCommit(ConnectionProxy.java:252)
        at io.seata.rm.datasource.ConnectionProxy.doCommit(ConnectionProxy.java:230)
        at io.seata.rm.datasource.ConnectionProxy.lambda$commit$0(ConnectionProxy.java:188)
        at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.doRetryOnLockConflict(ConnectionProxy.java:343)
        ... 60 more

看到“LockWaitTimeoutException: Global lock wait timeout” 我以為是有資源競爭,導致加鎖等待超時。但這個疑慮很快被打消了,因為這是必現的一個問題,每次執行到這個方法都報錯,甚至在下班後系統沒有人使用的情況下,我一點,還是報這個錯,這個時候可以確定就我一個人在用,而且查了資料庫沒有被鎖定的數據和事務,所以應該不是資源競爭導致的獲取鎖等待超時。

於是,我開始翻源碼

數據源被代理,本地事務提交走的是io.seata.rm.datasource.ConnectionProxy#commit()

doCommit()方法是放在io.seata.rm.datasource.ConnectionProxy.LockRetryPolicy#execute()中執行的

由於我們這裡client.rm.lock.retryPolicyBranchRollbackOnConflict配置的是false,所以這裡失敗後會重試,如果是true,則不重試

看到這裡,我們找到了“Global lock wait timeout”的出處了,原來是因為doCommit()執行過程中拋異常了,再重試次數用完後就會拋出LockWaitTimeoutException。因此,LockWaitTimeoutException只是表象,並不是最根本的原因,根本原因是doCommit()報錯了。

接著doCommit()看,我們知道,分支事務提交要先註冊,註冊成功後才能提交。而註冊就是要獲取全局鎖。

通過觀察DEBUG日誌,發現保存業務數據部分的分支註冊都是成功的

日誌太多,截取關鍵部分,如圖所示

結合代碼,發現真正的報錯發生在調用遠程服務啟動工作流那裡

查看工作流相關服務的日誌,發現一開始分支註冊就失敗了,部分關鍵日誌如下

工作流那個服務裡面,分支註冊返回的信息是:Global lock acquire failed xid = ....

幸好之前讀過Seata的源碼,不然此時肯定手足無措

於是,翻開Seata Server的源碼,看看為什麼返回的消息是這樣的

直接快進到io.seata.server.transaction.at.ATCore#branchSessionLock()

具體參見我的另一篇博文 https://www.cnblogs.com/cjsblog/p/16878067.html

在這裡,我們找到了“Global lock acquire failed”這個報錯信息的出處

證明,在執行branchSession.lock(autoCommit, skipCheckLock)的時候要麼失敗返回false,要麼拋異常了

根據配置,這裡是db,所以是DataBaseLockManager

接下來進入到LockStoreDataBaseDAO#acquireLock()開始真正加鎖了(往表裡插數據)

io.seata.server.storage.db.lock.LockStoreDataBaseDAO#acquireLock(java.util.List<io.seata.core.store.LockDO>, boolean, boolean)

方法太長,不細看了,重點看加鎖的SQL語句

由於用的MySQL,所以是io.seata.core.store.db.sql.lock.MysqlLockStoreSql

最終拼接好的SQL是這樣的:

insert into lock_table (xid, transaction_id, branch_id, resource_id, table_name, pk, row_key, gmt_create, gmt_modified) values (?, ?, ?, ?, ?, ?, ?, now(), now(), ?)

如果插入成功,則返回true,表示加鎖成功,對應的分支事務獲取鎖成功,分支事務註冊成功,皆大歡喜

補充一下,這裡面有很多地方配置項

至此,整個分支事務獲取鎖的邏輯我們都清楚了

接下來,再回頭看看lock_table表的各個列,首先看看怎麼從客戶端傳過來的一個lockKey變成List<LockDO>的

因此,假設客戶端發過來的lockKey是這樣:

offer message: xid=10.222.248.60:8091:2900686326154883760,branchType=AT,resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow,lockKey=wkfl_app_auth:12326192,12326193;act_ge_bytearray:6515890,6515891;act_re_procdef:rediscountClickSubmitCancel_UserTask_0yze6zf_5:1:6515892;act_re_deployment:6515889

 那麼這裡得到的List<LockDO>就是這樣的:

LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=wkfl_app_auth, pk=12326192, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^wkfl_app_auth^^^12326192)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=wkfl_app_auth, pk=12326193, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^wkfl_app_auth^^^12326193)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=act_ge_bytearray, pk=6515890, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^act_ge_bytearray^^^6515890)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=act_ge_bytearray, pk=6515891, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^act_ge_bytearray^^^6515891)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=act_re_procdef, pk=rediscountClickSubmitCancel_UserTask_0yze6zf_5:1:6515892, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^act_re_procdef^^^rediscountClickSubmitCancel_UserTask_0yze6zf_5:1:6515892)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=act_re_deployment, pk=6515889, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^act_re_deployment^^^6515889)

往lock_table表裡就會插入這6條數據,最後查看Seata服務端日誌發現,是由於欄位長度問題,導致插入失敗,於是加鎖失敗

原來pk欄位長度只有32,row_key欄位長度只有128,修改後的只讀長度如上圖所示

 

最後的最後,補充一個知識點

1、在整個全局事務中,每條SQL語句執行的時候都是一樣的流程,先註冊獲取全局鎖,然後才能提交,註意是每條SQL

2、所有的RM在執行本地操作的時候都是一樣的流程,因為數據源被Seata代理,所以在執行各自本地的邏輯時,設計到資料庫操作的,都是首先更改連接為非自動提交,然後進行分支註冊,註冊成功後連接可以提交了,最後報告分支狀態。

3、分支註冊會傳lockKey,註冊的過程就是獲取全局鎖的過程,也就是對這些lockKey包含的數據加鎖的過程。如果store.lock.mode=db的話,就是向lock_table表插數據。

4、在整個全局事務執行過程中,有多少次資料庫操作就有多少次分支註冊、提交、報告。因為每次跟資料庫的交互都要先獲取Connection,最終獲取到的都是ConnectionProxy

5、 所有RM(Resource Manager)本地事務都提交成功的話,整個全局事務算是提交成功了

Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeUpdate();

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Spring Boot 應用,在啟動的時候,如果想做一些事情,比如預先載入並緩存某些數據,讀取某些配置等等。總而言之,做一些初始化的操作時,那麼 Spring Boot 就提供了兩個介面幫助我們實現。 ...
  • L2-001 緊急救援 分數 25 作為一個城市的應急救援隊伍的負責人,你有一張特殊的全國地圖。在地圖上顯示有多個分散的城市和一些連接城市的快速道路。每個城市的救援隊數量和每一條連接兩個城市的快速道路長度都標在地圖上。當其他城市有緊急求助電話給你的時候,你的任務是帶領你的救援隊儘快趕往事發地,同時, ...
  • 使用 VLD 記憶體泄漏檢測工具輔助開發時整理的學習筆記。本篇介紹 VLD 配置文件中配置項 ReportFile 的使用方法。 ...
  • 項目練習01 1.項目介紹 這是一個簡單的項目練習,用於掌握新學習的SpringBoot技術。 項目操作界面 ● 技術棧 Vue3+ElementPlus+Axios+MyBatisPlus+SpringBoot 前後端分離 前後端分離開發,前端主體框架 Vue3 + 後端基礎框架 SpringBo ...
  • 什麼是Base64 Base64編碼是將字元串以每3個8比特(bit)的位元組子序列拆分成4個6比特(bit)的位元組(6比特有效位元組,最左邊兩個永遠為0,其實也是8比特的位元組)子序列,再將得到的子序列查找Base64的編碼索引表,得到對應的字元拼接成新的字元串的一種編碼方式。 每個3位8比特數據拆分成 ...
  • 藍橋杯【答疑】 題目描述 分析 這是一個貪心演算法,要所得的時刻之和最小,而且下一個同學需要等上一個同學結束以後才能進行,因此需要對所耗總時間進行有小到大的排序,總時間相同的同學則對前兩步時間之和有小到大進行排序,最後算出時間之和即可。 代碼 import java.util.Arrays; impo ...
  • 附件用的fastdf上傳和下載的, 本地開發時就沒考慮過多文件上傳就會有併發的問題,比如多個只上傳成功了一個或者上傳了但是文檔內容缺失了,變成0位元組。 呵。。都是一次難忘的經歷。 經過本地模擬大批量的上傳下載, 發現fastdf是在啟動時就初始化了tracker和stroge, 每次調用過他的介面後 ...
  • 一、Jx9 虛擬機的生命周期 載入 Jx9 腳本 jx9_compile() 或 jx9_compile_file(),載入編譯成功後,Jx9 引擎將自動創建一個實例 (jx9_vm) 並且返回指向此虛擬機的指針用於後續調用。 如載入編譯 Jx9 腳本時出現問題,也就是編譯時出錯,可調用jx9_co ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...