MySQL死锁分析与解决之路

死锁问题,这实在是个令人非常头痛的问题。本文将会对死锁进行相应介绍,对常见的死锁案例进行相关分析与探讨,以及如何去尽可能避免死锁给出一些建议。

死锁是并发系统中常见的问题,同样也会出现在数据库MySQL的并发读写请求场景中。当两个及以上的事务,双方都在等待对方释放已经持有的锁或因为加锁顺序不一致造成循环等待锁资源,就会出现“死锁”。常见的报错信息为 ” Deadlock found when trying to get lock... ”。

如上图,是右侧的四辆汽车资源请求产生了回路现象,即死循环,导致了死锁。

a.两个或者两个以上事务

c.锁资源同时只能被同一个事务持有或者不兼容

说明:后续内容实验环境为 5.7 版本,隔离级别为 RR(可重复读)

为了分析死锁,我们有必要对 InnoDB 的锁类型有一个了解。


  • 不同事务可以同时对同一行记录加 S 锁。
  • 如果一个事务对某一行记录加 X 锁,其他事务就不能加 S 锁或者 X 锁,从而导致锁等待。

  • T2 请求 S 锁立即被允许,结果 T1 T2 都持有 r 行的 S 锁
  • T2 请求 X 锁不能被立即允许

间隙锁( gap lock )

  • 如果索引列是唯一索引,那么只会锁住这条记录(只加行锁),而不会锁住间隙。
  • 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么依然会加间隙锁。

next-key lock 实际上就是 行锁+这条记录前面的 gap lock 的组合。假设有索引值10,11,13和 20,那么可能的 next-key lock 包括:

(10,11]

(13,20]

在 RR 隔离级别下,InnoDB 使用 next-key lock 主要是防止幻读问题产生。

InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在。为了支持在不同粒度上的加锁操作,InnoDB 支持了额外的一种锁方式,称之为意向锁( Intention Lock )。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。意向锁分为两种:

  • 由于 InnoDB 存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫描以外的任何请求。表级意向锁与行级锁的兼容性如下所示:


插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。假设某列有索引值2,6,只要两个事务插入位置不同(如事务 A 插入3,事务 B 插入4),那么就可以同时插入。

横向是已持有锁,纵向是正在请求的锁:


一个温馨小提示: xmen 平台支持查看死锁主库的死锁日志,访问方式如下:

在进行具体案例分析之前,咱们先了解下如何去读懂死锁日志,尽可能地使用死锁日志里面的信息来帮助我们来解决死锁问题。

MySQL 5.7 事务隔离级别为 RR

测试用例如下:


日志分析如下:

TRANSACTION 2322, ACTIVE 6 sec starting index read

locked 1 表示表上有一个表锁,对于 DML 语句为 LOCK_IX

LOCK WAIT 表示正在等待锁,2 lock struct(s) 表示 trx->trx_locks 锁链表的长度为2,每个链表节点代表该事务持有的一个锁结构,包括表锁,记录锁以及自增锁等。本用例中 2locks 表示 IX 锁和lock_mode X (Next-key lock)

MySQL thread id 37, OS thread handle 140445500716800, query id 1234 127.0.0.1 root updating

delete from student where stuno=5 表示事务1正在执行的 sql,比较难受的事情是 show engine innodb status 是查看不到完整的 sql 的,通常显示当前正在等待锁的 sql。

RECORD LOCKS space id 11 page no 5 n bits 72 index idx_stuno of table `cw`.`student` trx id 2322 lock_mode X waiting

事务2的 log 和上面分析类似:

RECORD LOCKS space id 11 page no 5 n bits 72 index idx_stuno of table `cw`.`student` trx id 2321 lock_mode X

| LOCK_gap,不过我们从日志里面看不到事务2执行的 delete from student where stuno=5;


RECORD LOCKS space id 11 page no 5 n bits 72 index idx_stuno of table `cw`.`student` trx id 2321 lock_mode X locks gap before rec insert intention waiting

--经典案例分析--

表结构和数据如下所示:



死锁日志如下所示:

重点说明下 delete 不存在的记录是要加上 gap 锁, 事务日志中显示lock_mode X locks gap before rec .

记录不存在,导致T2先持有了( lock_mode X locks gap before rec ) 锁住

2. T1 的 delete 与 T1 的 delete 一样同样申请了( lock_mode X locks gap but rec ) 锁住了

3. T1 的 insert 语句申请插入意向锁,但是插入意向锁和 T2 持有的 X gap ( lock_mode X locks gap before rec ) 冲突,故等待 T2 中的 gap 锁释放。

总结来说,就是 T1 (insert) 等待 T2 (delete) , T2 (insert) 等待 T1 (delete) 故而循环等待,出现死锁。

表结构和数据如下所示:



死锁日志如下:


1.事务 T2 insert into t7(id,a) values (26,10) 语句 insert 成功,持有 a=10 的 排他行锁( X

2.事务 T1 insert into t7(id,a) values (30,10), 因为T2的第一条 insert 已经插入 a=10 的记录,

( 即 lock mode S waiting ) 这是一个间隙锁会申请锁住(,10],(10,20]之间的 gap 区域。

[4,10]之间, 故需事务 T2 的第二条 insert 语句要等待事务 T1 的 S-Next-key Lock 锁释放,

表结构和数据如下所示:

死锁日志:

首先要理解的是 对同一个字段申请加锁是需要排队的。

(1). T2 执行 select for update 操作持有记录 id=30 的主键行锁:PRIMARY of table `test`.`tx` trx id 2077 lock_mode X locks rec but not gap。

(3). T2 执行根据主键 id=30 删除记录,需要申请 id=30 的行锁以及 c1=5 的索引行锁。但是 T1 以及持有该锁, 故会出现 index idx_c1 of table `test`.`tx` trx id 2077 lock_mode X locks rec but not gap waiting .

案例四:先 update 再 insert 的并发死锁问题

测试用例如下:

死锁分析:

--如何尽可能避免死锁--

锁竞争。

3.避免大事务,尽量将大事务拆成多个小事务来处理,小事务发生锁冲突的几率也更小。

务 B 更新数据的顺序为 2,1。这样更可能会造成死锁。

update 语句,如果是在事务里(运行了 start transaction 或设置了autocommit 等于0),

6.尽量按主键/索引去查找记录,范围查找增加了锁冲突的可能性,也不要利用数据库做一些

这样的语句,由于类似这样的语句用不到索引,因此将导致整个表的数据都被锁住。

分解为多个简单的 SQL。

版权声明:转载请附上原文出处链接及本声明。下载相关视频学习资料请到尚硅谷官方网站。


请使用浏览器的分享功能分享到微信等