群里說過幾百遍的長事務死鎖問題還是被人遇到了~別再這樣做了!
作者:飄渺Jam
近期測試中,發現幾年前開發的業務流程申請模塊在頻繁操作時會出現異常提示,導致審批流程失敗。最初以為是代碼邏輯不周或異常處理不足等常見錯誤,但通過日志排查后發現,問題源自數據庫的死鎖。
問題背景
近期測試中,發現幾年前開發的業務流程申請模塊在頻繁操作時會出現異常提示,導致審批流程失敗。最初以為是代碼邏輯不周或異常處理不足等常見錯誤,但通過日志排查后發現,問題源自數據庫的死鎖。以下是日志信息:
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)
根據日志可以清晰的看到一個事務在獲取鎖時發生了死鎖。
分析 MySQL 錯誤日志
進一步分析 MySQL 錯誤日志,發現以下死鎖情況:
圖片
事務一:
- 事務 ID: 6536211
- 狀態: 活躍 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')
事務二:
- 事務 ID: 6536206
- 狀態: 活躍 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 最終決定回滾事務 2(事務 ID: 6536206)以解決死鎖問題。
死鎖原因
死鎖發生的原因在于:
- 事務 1 和事務 2 都在嘗試更新 record_process_audit_apply_main_data 表中的記錄。
- 事務 1 正在等待事務 2 持有的鎖,事務 2 也在等待事務 1 持有的鎖,導致了死鎖。
定位代碼
通過進一步分析代碼,發現以下關鍵方法:submitApplication。該方法及其調用的其他方法都在同一個事務中執行,可能導致事務時間過長,增加了鎖競爭和死鎖的風險。核心代碼如下所示:
圖片
圖片
圖片
圖片
優化思路:
- 拆分事務:將大事務拆分成多個小事務,每個事務只處理一部分邏輯。確保每個事務盡可能短,減少鎖持有時間。
- 異步處理:使用異步任務處理耗時操作,如調用第三方接口。使用消息隊列將部分操作異步化。
- 優化數據庫操作:確保所有涉及的表都有適當的索引,減少查詢和更新的時間。使用批量操作,減少與數據庫的交互次數。
- 事務隔離級別:根據業務需求選擇合適的事務隔離級別,減少鎖的競爭。
- 日志記錄:記錄事務的開始和結束時間,以及關鍵操作的執行情況,便于問題排查。
優化后的偽代碼:
@Transactional
public void submitApplication(Application application) {
// 拆分事務,確保每個事務盡量短
try {
processApplicationDetails(application); // 處理申請細節
updateTaskStatus(application); // 更新任務狀態
notifyUser(application); // 通知用戶
} catch (Exception e) {
log.error("Error in submitApplication", e);
throw new RuntimeException("Application submission failed");
}
}
// 異步處理耗時任務
@Async
public void notifyUser(Application application) {
// 異步通知用戶
notificationService.sendNotification(application.getUserId(), "Your application is processed.");
}
責任編輯:武曉燕
來源:
JAVA日知錄