spring事務隔離導致數據庫連接耗盡而死鎖

記一次壓測排查死鎖

概述

應用背景

應用是公司內部的基礎設施平臺,會接收到多個內部平臺的數據上報,考慮到后期可能接入平臺增多,故對應用展開壓力測試查看應用的在高并發情況下表現。壓測接口主要是上報數據的接口,觀察接口的穩定性。

環境

  • 機器:2-core、4G DGRAM + 30G Disk

  • JDK1.8,-xms 2G -xmx2G

  • Tomcat

    Tomcat接受請求過程:在accept隊列中接收連接(當客戶端向服務器發送請求時,如果客戶端與OS完成三次握手建立了連接,則OS將該連接放入accept隊列);在連接中獲取請求的數據,生成request;調用servlet容器處理請求;返回response。

    accept-count為socket請求連接隊列數量,maxThreads為線程池最大數量,maxConnections為最大同時連接數

    • 接受和處理的最大連接數maxConnections:500
    • 請求處理最大線程數maxThreads:28
    • 隊列長度accpet-count:200(默認值為100)
  • druid

    • 初始大小initial-size: 2
    • 最大數據庫連接數max-active:20
    • 最小空閑數量min-idle:2
    • 最大等待毫秒數max-wait:600

測試指標

  • JVM內存運行穩定,無OOM,沒有不合理大對象
  • CPU、內存、網絡、磁盤、文件句柄占用平穩
  • 無頻繁線程鎖、線程數平穩
  • 業務線程負載均衡
  • 異常率小于0.1%

現象

TPS:Transaction Per Second 事務每秒

QPS: Query Per Second 請求每秒

當用戶一次操作(一個連接)只請求一個接口,TPS和QPS沒有任何區別

當TPS達到30左右,無論如何增添線程數(用戶數),TPS不會再上升

排查

  1. 首先進行top,系統負載Load avg平穩,CPU使用率平穩(不高),一般計算密集型應用 CPU 使用率偏高 load 偏低,IO 密集型相反。內存占用在70%左右,相對平穩。

  2. 使用jstat -gcutil查看沒有頻繁fullGC youngGC。

至此,我開始懷疑是不是代碼的質量寫的有問題。

  1. 接著按慣例我還是再用jstack查看堆棧,結果發現好多個線程在Waiting狀態,從下往上查看,主要是在申請數據庫連接時候getConnection()。

在最開始,有多個線程在獲取數據源時候卡住,導致數據庫連接池連接被占滿,而后所有線程全部處于等待狀態,引發死鎖。

最后定位到id生成器的一段代碼上面去

image-20200619145716098

然后向上定位,發現原來事務級別為3,REQUERIES-NEW,最后發現代碼位于自定義的ID生成器上面

image-20200619145955568

先概括一下死鎖原因,Spring事務傳播引發連接池死鎖。

當服務需要分布式id時,會首先從數據庫中獲取一個start_id,然后將start_id更新成start_id+step。那么從start_id~start_id+step段內對的所有id,都屬于當前這個服務了。如果start_id用完了,就會按照相同的流程重新申請一個start_id。

線程本身開啟事務(每個事務占用一個數據庫連接),然后使用id生成器申請Id,Id生成器發現Id不夠用,于是再開啟一個事務向數據庫拿id,發現連接不夠用了,于是等待連接池別的線程釋放連接,而別的線程也在等待id生成器的id,形成互相等待局面——死鎖。

Spring事務級別3,其實是TransactionDefinition.PROPAGATION_REQUIRES_NEW:創建一個新的事務,如果當前存在事務,則把當前事務掛起。也就是說無論如何都創建一個事務

解決

  1. 改變事務隔離級別,PROPAGATION_REQUIRES_NEWPROPAGATION_REQUIRED

    • PROPAGATION_REQUIRED(默認):如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。
  2. 增加連接池 getConnection 最大等待時間的配置。

    如果沒有獲取到連接一定時間則會拋出異常,結束這個線程。至于如何配置,不同的連接池的配置項不同,具體可參考對應的連接池官方文檔配置。如果防止部分連接執行時間太長或者數據源泄露,還可以加上Connection最大存活時間配置。

  3. 不使用同一個數據庫連接池

    正常來說,id生成的數據庫實例應該單獨配置實例

  4. 增加事務超時時間配置。(一般情況下不推薦,因為如果sql執行時間超過了超時時間,事務也會等待對應的sql執行完后結束,而在下一次執行sql時候報錯)

    通過spring事務注解時候,加上超時時間的屬性配置。

    @Transactional(timeout = 60)     //代表事務60秒超時
    

本文純粹是作者對工作中同小組遇到的壓測排查記錄,感謝同事wangxi

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。