群里說(shuō)過(guò)幾百遍的長(zhǎng)事務(wù)死鎖問(wèn)題還是被人遇到了~別再這樣做了!
問(wèn)題背景
近期測(cè)試中,發(fā)現(xiàn)幾年前開發(fā)的業(yè)務(wù)流程申請(qǐng)模塊在頻繁操作時(shí)會(huì)出現(xiàn)異常提示,導(dǎo)致審批流程失敗。最初以為是代碼邏輯不周或異常處理不足等常見錯(cuò)誤,但通過(guò)日志排查后發(fā)現(xiàn),問(wèn)題源自數(shù)據(jù)庫(kù)的死鎖。以下是日志信息:
SQL: UPDATE record_process_audit_apply_main_data SET update_account=?, update_name=?, update_time=?, task_node=?,apply_time=? WHERE (task_id = ?)
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
; Deadlock found when trying to get lock; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:271)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:70)
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:91)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:441)
at com.sun.proxy.$Proxy174.update(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.update(SqlSessionTemplate.java:288)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:64)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy$PlainMethodInvoker.invoke(MybatisMapperProxy.java:148)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:89)
at com.sun.proxy.$Proxy603.update(Unknown Source)
at sun.reflect.GeneratedMethodAccessor723.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
at com.sun.proxy.$Proxy604.update(Unknown Source)
at com.ifly.pdm.impl.recordaudittask.RecordProcessAuditTaskServiceImpl.handleCreateAuditTableData(RecordProcessAuditTaskServiceImpl.java:186)
根據(jù)日志可以清晰的看到一個(gè)事務(wù)在獲取鎖時(shí)發(fā)生了死鎖。
分析 MySQL 錯(cuò)誤日志
進(jìn)一步分析 MySQL 錯(cuò)誤日志,發(fā)現(xiàn)以下死鎖情況:
圖片
事務(wù)一:
- 事務(wù) ID: 6536211
- 狀態(tài): 活躍 10 秒,正在獲取行鎖
- 操作: 更新 record_process_audit_apply_main_data 表
- 等待的鎖: 等待主鍵索引上的記錄鎖
- sql: UPDATE record_process_audit_apply_main_data SET update_account='dxwang', update_name='汪冬雪', update_time='2024-11-15 15:17:34.216', task_node='16' WHERE (task_id = '1857321847525548034')
事務(wù)二:
- 事務(wù) ID: 6536206
- 狀態(tài): 活躍 5 秒,正在開始索引讀取
- 持有的鎖: 表 pdm.record_process_audit_apply_main_data 的主鍵索引上的記錄鎖。
- 等待的鎖: 正在等待在表 pdm.record_process_audit_apply_main_data 的主鍵索引上的記錄鎖
- sql: UPDATE record_process_audit_apply_main_data SET update_account='dywang3', update_name='王冬艷', update_time='2024-11-15 15:17:38.009', task_node='15', apply_time='2024-11-15 15:17:33.085' WHERE (task_id = '1857322026072875010')
MySQL 最終決定回滾事務(wù) 2(事務(wù) ID: 6536206)以解決死鎖問(wèn)題。
死鎖原因
死鎖發(fā)生的原因在于:
- 事務(wù) 1 和事務(wù) 2 都在嘗試更新 record_process_audit_apply_main_data 表中的記錄。
- 事務(wù) 1 正在等待事務(wù) 2 持有的鎖,事務(wù) 2 也在等待事務(wù) 1 持有的鎖,導(dǎo)致了死鎖。
定位代碼
通過(guò)進(jìn)一步分析代碼,發(fā)現(xiàn)以下關(guān)鍵方法:submitApplication。該方法及其調(diào)用的其他方法都在同一個(gè)事務(wù)中執(zhí)行,可能導(dǎo)致事務(wù)時(shí)間過(guò)長(zhǎng),增加了鎖競(jìng)爭(zhēng)和死鎖的風(fēng)險(xiǎn)。核心代碼如下所示:
圖片
圖片
圖片
圖片
優(yōu)化思路:
- 拆分事務(wù):將大事務(wù)拆分成多個(gè)小事務(wù),每個(gè)事務(wù)只處理一部分邏輯。確保每個(gè)事務(wù)盡可能短,減少鎖持有時(shí)間。
- 異步處理:使用異步任務(wù)處理耗時(shí)操作,如調(diào)用第三方接口。使用消息隊(duì)列將部分操作異步化。
- 優(yōu)化數(shù)據(jù)庫(kù)操作:確保所有涉及的表都有適當(dāng)?shù)乃饕?,減少查詢和更新的時(shí)間。使用批量操作,減少與數(shù)據(jù)庫(kù)的交互次數(shù)。
- 事務(wù)隔離級(jí)別:根據(jù)業(yè)務(wù)需求選擇合適的事務(wù)隔離級(jí)別,減少鎖的競(jìng)爭(zhēng)。
- 日志記錄:記錄事務(wù)的開始和結(jié)束時(shí)間,以及關(guān)鍵操作的執(zhí)行情況,便于問(wèn)題排查。
優(yōu)化后的偽代碼:
@Transactional
public void submitApplication(Application application) {
// 拆分事務(wù),確保每個(gè)事務(wù)盡量短
try {
processApplicationDetails(application); // 處理申請(qǐng)細(xì)節(jié)
updateTaskStatus(application); // 更新任務(wù)狀態(tài)
notifyUser(application); // 通知用戶
} catch (Exception e) {
log.error("Error in submitApplication", e);
throw new RuntimeException("Application submission failed");
}
}
// 異步處理耗時(shí)任務(wù)
@Async
public void notifyUser(Application application) {
// 異步通知用戶
notificationService.sendNotification(application.getUserId(), "Your application is processed.");
}