2.5.8. 死锁
死锁的发生:两个事务都陷入了等待状态,相互等对方释放锁,就形成了死锁。
为什么会产生死锁?
插入意向锁与间隙锁是冲突的,当其它事务持有该间隙的间隙锁时,需要等待其它事务释放间隙锁之后,才能获取到插入意向锁。而间隙锁与间隙锁之间是兼容的,所以两个事务中 select ... for update 语句并不会相互影响。
为什么间隙锁与间隙锁之间是兼容的?
间隙锁的意义只在于阻止区间被插入,因此是可以共存的。一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁,共享和排他的间隙锁是没有区别的,他们相互不冲突,且功能相同,即两个事务可以同时持有包含共同间隙的间隙锁。
但要注意,next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。
Insert 语句是怎么加行级锁的?
- 遇到唯一键冲突
在插入新记录时,插入了一个与「已有记录的主键或者唯一二级索引列值相同」的记录,此时插入就会失败,然后对于这条记录加上了 S 型的锁,隔离级别是可重复读的情况下。
- 如果主键索引重复,插入新记录的事务会给已存在并且主键值重复的聚集索引记录添加 S 型记录锁。
- 如果唯一二级索引重复,插入新记录的事务都会给已存在的二级索引列值重复的二级索引记录添加 S 型 next-key 锁。
如何避免死锁?
死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件死锁就不会成立。
有两种策略通过「打破循环等待条件」来解除死锁状态:
- 设置事务等待锁的超时时间。当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的,默认值时 50 秒。
当发生超时后,就出现下面这个提示:
- 开启主动死锁检测。主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑,默认就开启。
当检测到死锁后,就会出现下面这个提示:
如何排查死锁?
排查死锁时,首先需要根据死锁日志分析循环等待的场景,然后根据当前各个事务执行的SQL分析出加锁类型以及顺序,逆向推断出如何形成循环等待,这样就能找到死锁产生的原因了。
锁监控:
遇到线上死锁问题时,应该第一时间获取相关的死锁日志。通过 show engine innodb status 命令来获取死锁信息,但是只能拿到最近一次的死锁日志。MySQL 提供了一套 InnoDb 的监控机制,用于周期性(每隔 15 秒)输出 InnoDb 的运行状态到 mysql 服务的标准错误输出(stderr)。默认情况下监控是关闭的,只有当需要分析问题时再开启,并且在分析问题之后,建议将监控关闭,因为它对数据库的性能有一定影响,另外每 15 秒输出一次日志,会使日志文件变得特别大。
InnoDb 的监控主要分为四种:标准监控(Standard InnoDB Monitor)、锁监控(InnoDB Lock Monitor)、表空间监控(InnoDB Tablespace Monitor)和表监控(InnoDB Table Monitor)。后两种监控已经基本上废弃了。
要获取死锁日志,需要开启 InnoDb 的标准监控,推荐将锁监控也打开,它可以提供一些额外的锁信息,在分析死锁问题时会很有用。开启监控的方法有两种:推荐使用系统参数的形式开启监控。
-- 开启标准监控
set GLOBAL innodb_status_output=ON;
-- 关闭标准监控
set GLOBAL innodb_status_output=OFF;
-- 开启锁监控
set GLOBAL innodb_status_output_locks=ON;
-- 关闭锁监控
set GLOBAL innodb_status_output_locks=OFF;
-- 记录死锁日志
set GLOBAL innodb_print_all_deadlocks=ON;
MySQL 提供了一个系统参数 innodb_print_all_deadlocks 专门用于记录死锁日志,当发生死锁时,死锁日志会记录到 MySQL 的错误日志文件中。
避免死锁
MySQL只解决了快照读语义下的幻读,当前读没有彻底解决。在工作过程中偶尔会遇到死锁问题,对于 MySQL 的 InnoDb 存储引擎来说,死锁问题是避免不了的,没有哪种解决方案可以说完全解决死锁问题,但是可以通过一些可控的手段,降低出现死锁的概率。
- 对索引加锁顺序的不一致很可能会导致死锁,所以尽量以相同的顺序来访问索引记录和表。在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能;
- Gap 锁往往是程序中导致死锁的真凶,由于默认情况下 MySQL 的隔离级别是 RR,如果能确定幻读和不可重复读对应用的影响不大,可以考虑将隔离级别改成 RC,可以避免 Gap 锁导致的死锁;
- 为表添加合理的索引,如果不走索引将会为表的每一行记录加锁,死锁的概率就会大大增大;
- MyISAM 只支持表锁,它采用一次封锁技术来保证事务之间不会发生死锁,所以,可以使用同样的思想,在事务中一次锁定所需要的所有资源,减少死锁概率;
- 避免大事务,尽量将大事务拆成多个小事务来处理;因为大事务占用资源多,耗时长,与其他事务冲突的概率也会变高;
- 避免在同一时间点运行多个对同一表进行读写的脚本,特别注意加锁且操作数据量比较大的语句;经常会有一些定时脚本,避免它们在同一时间点运行;
- 设置锁等待超时参数:innodb_lock_wait_timeout,这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。通过设置合适的锁等待超时阈值,可以避免这种情况发生。