下单接口从晚上八点开始报错,一晚上攒了两百多条。日志里是Deadlock found when trying to get lock; try restarting transaction,错误号1213。同一张订单表,白天跑一整天都没有出现过一次。库是8.0,隔离级别没动过,还是默认的REPEATABLE READ。
死锁的条件是两边各持一把锁,又都在等对方手里那把。InnoDB检测到环路时不会让两边一起等,它挑一个事务回滚,把错误抛给客户端。被回滚的那个就是日志里的牺牲品,另一个照常提交,所以同一个接口有时成功有时失败。
SHOW ENGINE INNODB STATUS的输出里有一段LATEST DETECTED DEADLOCK。它只保留最近一次,后面的会覆盖前面的,报错之后要尽快抓。执行完把结果存成文件,别在终端里翻。这一段开头两行是发生时间和两个事务的ID,往下是各自的等待状态。两个事务ID你要抄下来,后面按它去对业务日志。
两个事务下面各有一段SQL,看它们先后锁了哪些行。第一个事务下面写着HOLDS THE LOCK(S),等锁的那段写着WAITING FOR THIS LOCK TO BE GRANTED,两边对着看就知道各自持有什么、在等什么。很多死锁在这一步就能定案,不需要再猜代码。带WHERE条件的UPDATE和DELETE是常客,SELECT FOR UPDATE也会。
按顺序相反是最常见的一类,事务A先改状态再改金额,事务B先改金额再改状态,两边执行到第二步就撞上。代码里两个方法各按自己的字段顺序拼SQL,单看都没问题,合起来就是一个环路。另一种不那么直观,出在二级索引上。主键之外的索引都算二级索引,语句走它时InnoDB先锁索引记录,再回表锁主键记录,两把锁分两步拿。两个事务走的索引不同,锁的先后顺序就不同,哪怕更新的是同一批主键。把语句改成走同一个索引,或者把范围收窄,环路就不容易形成。
改代码时守住四条,所有事务按同一顺序访问同一批行;事务里不要夹带网络调用;WHERE条件走索引,别让扫描范围铺满整张表。第三条最容易被忽略,范围一大,锁的行数跟着涨,撞上的概率成倍往上走。还要短,一个事务里包的东西越多,持锁时间越长。
重试要写在业务层。捕获1213和1205这两个错误号,退避几十毫秒再试,次数限在两三次。直接while循环重试会把连接池拖死。重试前最好重新读一次数据,别拿旧值再写一遍。
这次的改动是把两个方法的更新顺序统一,都先改状态再改金额。改完观察三天,1213从每晚两百多条降到个位数。剩下的几次是另一个批量任务,它按主键区间扫,和在线事务撞在一起,把区间切成小块之后也消失了。死锁不会彻底消失,能压到不影响业务就算处理完。
申请创业报道,分享创业好点子。点击此处,共同探讨创业新机遇!
