1. 問題
線上一個服務(wù),放量之后出現(xiàn)比較多的 long sql。
經(jīng)過分析捣鲸,問題出在以下邏輯:
- 主線程每次拉取一批任務(wù)并檢查任務(wù)是否已經(jīng)存在
- 如果存在則準(zhǔn)備執(zhí)行;否則將任務(wù)插入 db处渣。然后準(zhǔn)備執(zhí)行這批任務(wù)
- 每個任務(wù)使用線程池的一個線程去執(zhí)行,在子線程中檢查任務(wù)是否符合條件蛛砰,符合就將任務(wù)狀態(tài)更改為“進行中”并執(zhí)行罐栈,否則直接結(jié)束,并在任務(wù)完成后更新為“已完成”
- 主線程等待1s泥畅,如果期間所有子任務(wù)都完成了荠诬,返回成功,否則超時結(jié)束位仁,待下次啟動再檢查這些任務(wù)是否已經(jīng)完成柑贞,沒完成的下次繼續(xù)。
慢有如下特點:
- long sql 都是更新狀態(tài)為“進行中”的sql聂抢,更新狀態(tài)為“已完成”的完全沒有 long sql
- long sql 的執(zhí)行時間幾乎都是1s
- 更新狀態(tài)為“進行中”的 sql钧嘶,也只有極少部分實際表現(xiàn)是 long sql,大部分都是正常執(zhí)行的
2. 原因
數(shù)據(jù)庫使用的是“讀已提交”的隔離級別琳疏。(如果是可重復(fù)讀也會出現(xiàn)這個問題)
主線程方法上加了 @Transactional 注解, 所以主線程的執(zhí)行是在一個事務(wù)中, 在創(chuàng)建任務(wù)插入 db 時會獲取對應(yīng)數(shù)據(jù)行的寫鎖有决;如果是之前已經(jīng)創(chuàng)建的任務(wù),則不會加鎖空盼。
子線程方法也加了 @Transactional 注解书幕。因為更新為“已完成”與更新為“進行中”都是在子線程的事務(wù)中,先更新為“進行中”如果已經(jīng)獲得了鎖揽趾,則更新為“已完成”不會再被阻塞
主線程等待 1s 后結(jié)束釋放鎖台汇,所以 long sql 都是 1s。
每次都是新創(chuàng)建任務(wù)占極少部分但骨,大部分都是執(zhí)行之前已經(jīng)創(chuàng)建的任務(wù)励七,而已經(jīng)創(chuàng)建的任務(wù)再次執(zhí)行不會被主線程阻塞,所以 long sql 比例極小奔缠。
3. 根本問題所在
db 操作放在了多線程中掠抬,而且都用 @Transactional 注解加了事務(wù),而各個線程的事務(wù)是不共享的校哎。
4. 解決問題的方法
- 將sql相關(guān)的操作抽出到子線程外两波,即所有db操作都在主線程內(nèi)執(zhí)行,缺點是代碼變復(fù)雜
- 改造 @Transactional 注解闷哆,讓事務(wù)能夠傳遞到線程池中腰奋。解決@Transactional不能跨線程池共享事務(wù)的問題—使用TransmittableThreadLocal