最近,公司現網的業務中出現上圖所示的死鎖異常,沿着問題分析,發現這個問題涉及很多數據庫的基礎知識。
背景:
使用數據庫:Mysql
涉及表格:t_invest
數據庫隔離級別:可重復讀(Repeatable Read)
死鎖場景:saveRepaymentInfo事務的A()方法對t_invest表執行如下update操作:
<update id = "A" parameterType = "java.util.List">
<foreach collecton = "list" item = "item" separator = ";">
update t_invest
set status = 1,
end_date = #{item.endDate},
period = #{item.periods},
update_time = now()
where invest_no = #{item.invest_no}
</foreach>
</update>
invest_no字段為設置了唯一索引
異常信息:
死鎖分析:
通過異常日志跟蹤定位到業務代碼,發現死鎖出現在syncRedeemApply事務。
事務syncRedeemApply會進行t_invest表的status的批量更新,批次最大數量為1000條。t_invest表的事務隔離級別為“可重復讀”,在該隔離級別下,每次執行更新操作時會對索引加行鎖,這個事務不存在多線程並發訪問的情況,推斷不是因為多程序並發操作造成的死鎖。
通過分析業務功能發現,其他分布式業務模塊在saveRepaymentInfo事務進行t_invest表的status的批量更新的同時,另外有一個updateExitStatusByBatch事務同時也在invest表進行批量更新,而且都是利用investno索引進行批量update,問題就出現在這里!
updateExitStatusByBatch事務進行批量更新的方法假設是B(),其批量更新語句為:
<update id = "B" parameterType = "java.util.List">
<foreach collecton = "list" item = "item" separator = ";">
update t_invest
set status = 1,
end_date = #{item.endDate},
period = #{item.periods},
update_time = now()
where invest_no = #{item.invest_no}
</foreach>
</update>
(其實兩個事務的方法A與B的sql是一樣的)
死鎖是因為兩個或兩個以上的線程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱為死鎖進程。
如下圖所示:
t_fixin_invest的事務隔離級別為“可重復讀”,每次執行更新操作時會對索引加行鎖,在一個事務內批量修改如果沒有全部修改完,索引是不會被放開的,亦即索引上的行鎖不會被放開,所以當兩個事務中同時進行更新時,如果有重復數據,有可能出現互相等待,從而爆死鎖。
如上圖,syncRedeemApply事務鎖住i1、i3、i5,updateExitStatusByBatch事務鎖住i2、i4、i6,syncRedeemApply事務執行到i2的update,等待updateExitStatusByBatch事務釋放i2的鎖,updateExitStatusByBatch事務執行到i1的updat,等待syncRedeemApply事務釋放i1的鎖,如此,兩個事務互相等待,造成死鎖。
我的解決辦法是這兩個事務在進行批量插入操作,先按主鍵進行排序,避免在插入時有重復數據出現互相等待,然后再進行批量插入操作!