群里说过几百遍的长事务死锁问题还是被人遇到了~别再这样做了!

数据库 其他数据库
近期测试中,发现几年前开发的业务流程申请模块在频繁操作时会出现异常提示,导致审批流程失败。最初以为是代码逻辑不周或异常处理不足等常见错误,但通过日志排查后发现,问题源自数据库的死锁。

问题背景

近期测试中,发现几年前开发的业务流程申请模块在频繁操作时会出现异常提示,导致审批流程失败。最初以为是代码逻辑不周或异常处理不足等常见错误,但通过日志排查后发现,问题源自数据库的死锁。以下是日志信息:

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日知录
相关推荐

2011-11-01 09:31:16

写代码程序需要进化

2023-10-05 18:49:12

.Net​Newtonsof源码

2024-01-10 09:44:11

MySQL死锁

2021-10-26 08:22:38

消息堆积扩容RocketMQ

2020-12-17 10:23:41

死锁LinuxLockdep

2019-02-13 08:58:11

程序员加薪开发

2015-07-30 09:48:38

自学编程3遍读书法

2017-01-15 10:20:57

交通网络

2013-07-26 09:16:13

SwiftOpenStackSwiftStack

2022-08-10 19:28:40

Hadoop数据库

2019-04-11 14:51:12

数据

2022-05-24 16:09:38

前端脚本

2021-10-29 07:49:22

Spring事务管理

2011-06-24 15:07:16

百度快照蜘蛛

2014-07-14 11:47:03

火狐浏览器

2020-04-24 20:05:16

VueAxios前端

2021-03-22 16:10:10

手机内存技术

2024-04-30 08:22:51

Figma图形编辑变换矩阵

2022-04-26 21:49:55

Spring事务数据库

2024-03-12 08:22:50

TypeScriptRust框架
点赞
收藏

51CTO技术栈公众号