记一次由@Transactional+线程池+Future共同引发的数据库死锁问题

故事背景
前段时间,提测前夜,某同学的代码出现了史无前例的数据库死锁问题,异常信息如下:
org.springframework.dao.CannotAcquireLockException:

Error updating database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction

The error may involve defaultParameterMap

The error occurred while setting parameters

SQL: DELETE FROM t_user WHERE user_id =?

Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction

; SQL []; Lock wait timeout exceeded; try restarting transaction; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction

Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
大概意思是等待获取数据库锁超时,目测跟事务有关系。由于当时已经凌晨,又急于解决问题,于是某同学把所有调用链的@Transaction都去掉,再试就好了,于是愉快的提测。

时隔多日,也没找出来具体原因是啥。于是,今天找当事人还原了一下事故现场,下面是一个大概的调用链:
A类有标注了@Transaction public方法a,作为请求入口,在此方法内对x表做update操作,然后调用同类的private方法b
private b对数据做了一些处理,然后用Executor对资源id做处理,将id传给B类的public c方法,并将Callable的Future对象返回
B类c方法同样标注了@Transactional,并对x表做delete操作
在A类 private b方法,收集到Future对象后,对List集合遍历做Future.get操作,阻塞到线程池的任务全部执行结束,继续后续的逻辑处理
于是,我按照这个调用链写了类似的A类,B类的a,b,c方法,执行单元测试,果然,错误复现。
事故原因
A类在执行update操作时持有x表的锁,此时A类的事务未提交。
A类a方法执行完update操作之后,调用A类 private方法b,方法b调用B类方法c,方法c创建了新的事务
B类方法c创建新的事务,想要对x表操作,就要获取x表的数据库锁,但是在A类paivate方法b中执行了Future.get操作,导致主线程阻塞,A类a方法的事务无法提交,由于a方法无法提交事务释放x表的锁,导致线程池的子线程无法获取x表的锁,因此双方进入了互相等待的局面。。。
事故还原
为了印证上面的事故原因,我分别对a,c方法加@Transactional标签以及分别执行Future.get操作,得到以下结果:


总结
由表格的结果可以看出,当a方法标注事务管理并b方法执行Future.get操作,会导致死锁问题。原因是a方法由于阻塞无法提交事务,就无法释放锁,而线程池的子线程(独立的事务)需要获取数据库锁才能继续执行,因此,导致了最终死锁的问题

你可能感兴趣的:(记一次由@Transactional+线程池+Future共同引发的数据库死锁问题)