我们的数据库中的发生死锁的表是具有”多列组合构建的唯一索引“(不包含自增的主键),且数据库的隔离等级为Read Committed,另外对于这个表来说是写入远大于读取的,由于业务的原因,经常会出现同一数据反复插入(同一数据指唯一索引值相同的数据,但其他非索引字段可能不同),所以为了简化代码,我们使用insert on duplicate key update来解决这种问题,当mysql检测到唯一键冲突时,仅更新特定(非索引)字段。但是问题就出现在大规模多worker并发插入的时候,会经常出现"Deadlock found when trying to get lock"。
即任务提交到线程池,多线程并发执行insert on duplicate key update,并且为长事务时,抛出下面异常:
java.lang.RuntimeException: org.springframework.dao.CannotAcquireLockException: ### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction### The error may involve com.kfang.service.price.dict.dao.automapper.DictGardenAppraisalMapper.insertDictGardenAppraisal-Inline### The error occurred while setting parameters### SQL: insert into kfang_price.t_dict_garden_appraisal ( ) values( ) ON DUPLICATE KEY UPDATE FTRUST_NUM = values(FTRUST_NUM), FTRUST_AVG_PRICE = values(FTRUST_AVG_PRICE), FTRUST_TOTAL_PRICE = values(FTRUST_TOTAL_PRICE), FTRUST_TOTAL_AREA = values(FTRUST_TOTAL_AREA), FTRADE_NUM = values(FTRADE_NUM), FTRADE_AVG_PRICE = values(FTRADE_AVG_PRICE), FTRADE_TOTAL_PRICE = values(FTRADE_TOTAL_PRICE), FTRADE_TOTAL_AREA = values(FTRADE_TOTAL_AREA), FOUTER_REFER_PRICE = values(FOUTER_REFER_PRICE)### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction; Lock wait timeout exceeded; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction二、图文说明:
一般定位死锁原因第一步就是执行”show engine innodb status“, 查看innodb Standard monitor输出结果,这里面会有数据库最后一次的死锁记录。会记录出现死锁的两个事务,它们分别在等待什么锁,并且手里持有什么锁。mysql在检测到发生死锁的时候,会随机回滚其中的一个事务,从而解开死锁。下面的截图是发生死锁的时候innodb status截图(和业务相关的数据已脱敏,这里均用column_n和value_n表示)
Transaction1:
Transaction2:
现象阐述:
从上方两个截图可以发现,死锁均发生在insert on duplicate key update语句执行的时候,并且每个insert语句均为批量插入多个数据。对于事务一,可以看到事务一在等待某个锁的获取,且这个锁是"lock_mode X locks gap before rec insert intention waiting",直接翻译过来就是插入意向锁在等待排他gap锁的释放,也就是只有排他gap锁释放后插入意向锁才能获取到(关于这些锁的含义见下一节)。对于事务二,同样可以看到相同的一句话。并且两个事务的锁冲突均发生在”唯一索引“上。再进一步观察可以看到,事务二所持有("Holds the Locks"下方展示的索引值)的排它锁所在的索引(锁均是加在索引上或者索引区间上的),与事务一等待获取锁的索引是一样的。进一步展示了的确,在同一个索引上出现了一个等待获取,一个已经获取的冲突现象。
三、相关概念:
1、在T1时刻Session1执行完insert操作后,由于插入的code=2已经存在于表中,发生了唯一键冲突,所以触发了duplicate-checking,导致在(1,3]这个区间加上了next-key lock。这里,我为了进一步证明确实只有(1,3]这个区间加了锁。在T1时刻执行完后,验证插入code=0/4/6的数据可以在Session2中执行成功。同时这个时候Session2中可以修改code=1的数据,如update test2 set other=0 where code=1可以执行成功(当然你不能update test2 set code=2 where code=1,因为这个操作是在向(1,3]的间隙内插入了数据,违反了gap锁的要求)。同时我们可以证明这时code=3肯定是被排他锁锁住的,由于当出现唯一键冲突时,就会执行on duplicate key update,更新other字段,所以code=3一定在更新结束后处于排它锁锁定状态(补充说明:可以证明如果是共享锁的话,session2在T2时刻执行insert into test2(code, other) values (3, 33)语句的话,一定会立刻包duplicate error而不会阻塞。但是事实上如果Session2在T2时刻执行这句sql,会一直阻塞,进一步说明code=3加的是排它锁。另外需要注意的是,其实我目前只能非常确定code = 3是有排它锁,但是(1,3)上面,到底是S gap lock 还是X gap lock无法确定,不过无论是S还是X,不影响后续的解释。)
2、在T2 完成时,同理也会在(3,5]这个区间上X next-key lock (在上面的截图中也可以看到插入code=5后,正在插入code=2的时候,写着HOLD the lock hex 80000005)