一次被自己蠢到的數據庫死鎖經歷
先說這次事件的背景故事,一個創業項目,需要冷啓動。該項目類似於微博的一個項目,所以冷啓動需要導入一批微博數據和微博評論數據,導入數據還需要在馬甲賬號加上統計數據,因爲可以查看他人中心,不然太假。就在更新馬甲賬號統計數據的時候老是發生死鎖。
技術背景,我開一個接口給爬蟲工程師上傳約定的標準json文件,由於考慮數據可能會比較多,所以用了一個線程池去插入數據,線程池大小16。僞代碼如下:
- Service類:
List<D> dataList = Json.toObj(json);
dataList.forEach(
v->{
threadPoolTaskExecutor.execute(() -> {
dataVHandler.handler(v);
});
}
)
- Handler類:
@Transactional
void method(data){
weiboRepository.save(data);
List<C> commentList = data.getCommentList();
commentList.forEach(c->{
commentRepository.save(c);
roleDataRepository.update(c.getUserId(), numData);
})
roleDataRepository.update(data.getUserId(), numData);
}
- UserDataRepository類:
@Transactional
void update(userId, numData){
D userData = selectByUserId(userId);
if(null == userData){
// 初始化且合併數據
merge(userData,numData);
save(userData);
}
// 合併數據
merge(userData,numData);
update(userData);
}
一開始,總看在後面這個小事物中,後來想想,userId未加索引,select 會掃描全表,但是select也沒有加上加鎖關鍵字,也不應該啊,爲了效率和防止出錯,給userId加上了索引,還有我數據事物隔離級別是讀提交,沒有間隙鎖,行鎖也不應該死鎖啊。結果當然是依舊死鎖。
各種分析,使用數據庫命令,查看死鎖結果,死鎖結果都是鎖記錄鎖(行鎖,這也說明不是間隙鎖),並且是兩個記錄之間互相等待死鎖,想了很久都沒想明白爲什麼。
於是問別人,其他人也不知道爲啥,但是得到了新的思路,有人提出了,可能更新連接不夠,Mybaits框架也會出現問題,報鎖等待超時異常。結果測試增加連接池,依據死鎖。
總想啊,到晚上睡覺前都沒找到原因,第二天到公司,繼續找原因,於是我一直導入同個數據,發現總是卡在某個數據,肯定是導入的時候文件中那行數據有問題,於是拿出來分析一下,瞬間腦子清醒了。爲啥呢?因爲兩條記錄的userId順序互相交叉了,一條微博下面有個評論列表,評論列表也有馬甲賬號id。然後再查一下Spring的事物傳播特性,默認是合併事物,所以上面代碼的事物是在handler的事物中執行,handler的事物更新了多個UserData,然後併發事物交叉更新,就死鎖了。最後在UserDataRepository類另起一個事物,最後測試一下,沒有死鎖OK了。修改後代碼如下:
@Transactional(propagation = Propagation.REQUIRES_NEW)
void update(userId, numData){
D userData = selectByUserId(userId);
if(null == userData){
// 初始化且合併數據
merge(userData,numData);
save(userData);
}
// 合併數據
merge(userData,numData);
update(userData);
}
死鎖原因(普通的交叉):
總結
在一個事物中多次更新同一個表的時候,一定要特別注意,因爲多次更新同一個表,就可能會出現交叉的情況,就會發生死鎖。
【參考】:
GC Ergonomics間接引發的鎖等待超時問題排查分析
記spring事務傳播機制引發連接池死鎖問題及解決方案
Mybatis-update - 數據庫死鎖 - 獲取數據庫連接池等待
Mybatis-update - 數據庫死鎖 - 獲取數據庫連接池等待