并发系统中不同线程出现对竞争资源的循环依赖并阻塞相互等待就会发生死锁
例如事务 A 中,执行 update test set k = k + 1 where id = 1; 会锁定 id 为 1 的记录
事务 B 中,执行 update test set k = k + 2 where id = 2; 会锁定 id 为 2 的记录
此时,如果在事务 A 中执行 update test set k = k + 3 where id = 2; 同时在事务 B 中执行 update test set k = k + 4 where id = 1;
两个事务会分别阻塞等待另一个事务占用的排他锁,从而陷入死锁
如何避免死锁
设置超时
设置锁等待超时是最为简单粗暴的办法,innodb 提供了加锁阻塞超时时间的设置:innodb_lock_wait_timeout
默认值是 50,即一个加锁请求在等待 50 秒后会自动返回加锁失败
但这样存在几个问题:
该配置项的单位是秒数,因此他的最小粒度是 1 秒,对于有些系统,1 秒的超时显然太长,而另一些系统中,1 秒的超时又显得太短,难以区分是正常的锁等待还是发生了死锁,从而可能造成误伤
主动死锁检测
innodb 提供了主动死锁检测机制,innodb 在锁冲突发生时,会扫描持有该锁或在竞争该锁的事务,判断他们之间是否有可能产生死锁,一旦发现当前事务的等待会产生死锁,那么就会立即返回错误
可以通过 innodb_deadlock_detect 设置为 on 或 off 来开启或关闭主动死锁检测机制,默认是开启状态
看上去主动死锁检测 + 业务重试可以解决所有的死锁问题了,但是这同样存在一定的问题
由于整个主动死锁检测过程需要循环遍历所有持有或等待锁的事务两两间的持有锁情况,所以这个过程的时间复杂度是 O(n^2),在高并发的场景下,例如有 1000 个并发的线程同时更新一行,虽然他们之间并不会产生死锁,但主动死锁检测却要进行 100 万次对比,最终造成 CPU 利用率的飙高
拆分字段实现单条记录并发度的下降
上述主动死锁检测引起性能问题的原因主要是单条记录加锁的并发度过高,但通常,我们不能靠降低系统的并发度来避免问题的发生,但我们可以通过横向或纵向拆分数据库中的字段来实现对并发加锁的优化
例如,对于单纯用于递增记录的字段,我们可以拆分成多个字段,每次随机选取某个字段进行递增的记录
这样虽然可以有效降低单个字段上的并发度,但依赖于实际的业务,如果业务场景同时存在增减操作,那么拆分成多个字段必须要考虑是否会将某个字段减到负数等问题,在很大程度上提升了业务逻辑的复杂度