MySQL 锁简介及死锁问题分析

Posted IT技术闲聊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL 锁简介及死锁问题分析相关的知识,希望对你有一定的参考价值。








1

mysql的锁



锁是计算机协调多个进程或线程并发访问某一资源的机制。锁保证数据并发访问的一致性、有效性;锁冲突也是影响数据库并发访问性能的一个重要因素。锁是Mysql在服务器层和存储引擎层的的实现并发控制的手段。
1InnoDB 实现了以下两种类型的行锁 - 读写锁
共享锁(S:又称读锁,允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
排他锁(X:又称写锁,允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
2 InnoDB 还有两种内部使用的意向锁(Intention Locks), 来允许行锁和表锁共存,实现多粒度锁机制,这两种意向锁都是 表锁 - 意向锁
意向共享锁(IS:事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
意向排他锁(IX:事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。
兼容关系

IX
IS
X
S
IX
兼容
兼容
冲突
冲突
IS
兼容
兼容
冲突
兼容
X
冲突
冲突
冲突
冲突
S
冲突
兼容
冲突
兼容

如果一个事务请求的锁模式与当前的锁兼容,
InnoDB 就将请求的锁授予该事务; 反之, 如果两者不兼容,该事务就要等待锁释放。
3)另外,按照算法不同,还可以分为:
RECORD LOCK行上的锁;
GAP LOCK:不锁记录,仅仅记录前面的Gap。当用范围条件而不是等值条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;但对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
NEXT-KEY LOCK:同时锁住记录(数据),并且锁住记录前面的Gap NEXT-KEY=RECORD LOCk + GAP LOCK

2

InnoDB加锁过程



事务隔离级别Read Committed (RC)Repeatable Read (RR)
InnoDB的加锁分析前提条件
前提一: 查询列是不是主键?
前提二: 当前系统的隔离级别是什么?
前提三: 查询列上有索引吗?
前提四: 查询列是唯一索引吗?
前提五: 两个SQL的执行计划是什么?索引扫描?全表扫描?
以下面update为例,说明Innodb加锁过程
update t1 set update_time = now() where k = 10 ;
组合一:RC隔离级别,k列是主键,
组合二:RC隔离级别,k列是二级唯一索引
组合三:RC隔离级别,k列是二级非唯一索引
组合四:RC隔离级别,k列上没有索引
组合五:RR隔离级别,k列是主键
组合六:RR隔离级别,k列是二级唯一索引
组合七:RR隔离级别,k列是二级非唯一索引
组合八:RR隔离级别,k列上没有索引
组合九: Serializable隔离级别
各组合加锁情况

组合一: Read Committed 隔离级别,k列是主键,给定SQLupdate t1 set update_time = now() where k = 10; 只需要将主键上 k = 10的记录加上X锁即可。

组合二: Read Committed 隔离级别,k列有unique索引,unique索引上的k=10一条记录加上X锁,同时,会根据读取到的列,回主键索引(聚簇索引),然后将聚簇索引上对应的主键索引项加X锁。

组合三: Read Committed 隔离级别,k列上有索引,那么对应的所有满足SQL查询条件的记录,都会被加锁。同时,这些记录在主键索引上的记录,然后将聚簇索引上对应的主键索引项加X锁。

组合四: Read Committed 隔离级别,若k列上没有索引,SQL会走聚簇索引的全扫描进行过滤,由于过滤是由MySQL Server层面进行的。因此每条记录,无论是否满足条件,都会被加上X锁。但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。同时,优化也违背了2PL的约束。

组合五: Repeatable Read 隔离级别,k列是主键列,给定SQL update t1 set update_time = now() where k = 10; 只需要将主键上 k = 10的记录加上X锁即可。

组合六: Repeatable Read 隔离级别,k列有unique索引,unique索引上的k=10一条记录加上X锁,同时,会根据读取到的列,回主键索引(聚簇索引),然后将聚簇索引上对应的主键索引项加X锁。

组合七Repeatable Read 隔离级别,k列有索引, 通过索引定位到第一条满足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录,此时,不需要加记录X锁,但是仍旧需要加GAP锁,最后返回结束。

考虑到B+树索引的有序性,满足条件的项一定是连续存放的。如果要插入一条记录,肯定会插入在相同位置,为了保证两次查询查到的值一致,MySQL选择了用GAP锁,将 查询值范围前、查询值范围、查询值范围后 三个GAP给锁起来。
GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
 k                | 7  | 8  | 10 | 10 | 40 | 50 |
primary id   | 1  | 2  | 3   | 4   | 5   | 6   |
为了保证[8,2][10,3]间,[10,3][10,4]间,[10,4][40,5]不会插入新的满足条件的记录,MySQL选择了用GAP锁,将这三个GAP给锁起来。

组合八 Repeatable Read隔离级别下,如果进行全表扫描的当前读,那么会锁上表中的所有记录,同时会锁上聚簇索引内的所有GAP,杜绝所有的并发 更新/删除/插入 操作

  聚簇索引上的所有记录,都被加上了X锁。其次,聚簇索引每条记录间的间隙(GAP),也同时被加上了GAP锁。

 组合九Serializable隔离级别下直接用加锁的方式来避免并行访问。

RCRR隔离级别下,都是快照读,不加锁。
Serializable隔离级别,读不加锁就不再成立,所有的读操作,都是当前读。

3

什么情况下会造成死锁



所谓死锁(DeadLock): 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
产生死锁的原因:两个(或以上) Session加锁的顺序不一致。

解决死锁的方法:让不同的session加锁有次序。

4

场景复现



数据库表:
CREATE TABLE `test` (
       `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
       `number` INT(11) UNSIGNED DEFAULT NULL,
       PRIMARY KEY (`id`),
       UNIQUE KEY `number` (`number`)
) ENGINE = INNODB AUTO_INCREMENT = 1 CHARSET = utf8;
 

表结构为,一个主键
id,另一个唯一索引number。表里的数据如下:
MySQL [gd_tttt]> select * from test;
+----+----------+
| id    | number |
+----+----------+
|  1    |      1    |
|  2    |      2    |
|  4    |      4    |
+----+--------+
3 rows in set (0.00 sec)

出现死锁的操作如下:
步骤
事务1
事务2
1

begin
2

delete from test where number = 2;
3
begin

4
delete from test where number = 2; (事务1等待)

5
提示出现死锁:ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
insert into test (id, number) values (10, 2);

5

问题排查



通过SHOW ENGINE INNODB STATUS; 来查看死锁日志:

先看事务2TRANSACTION 67447783),
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 6357 page no 5 n bits 72 index number of table `gd_tttt`.`test` trx id 67447783 lock_mode X locks rec but not gap
Record lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 3

事务
2删除number=2的记录,为record lock行锁,非间隙锁。占用X锁。
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 6357 page no 5 n bits 72 index number of table `gd_tttt`.`test` trx id 67447783 lock mode S waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 32

事务
2插入number=2的记录,S锁等待(insert语句在普通情况下是会申请排他锁,也就是X锁,但是这里出现了S锁。这是因为number字段是唯一索引,所以insert语句会在插入前进行一次duplicate key的检查,为了使这次检查成功,需要申请S锁防止其他事务对a字段进行修改。)
再来看事务1TRANSACTION 67447844),
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 6357 page no 5 n bits 72 index number of table `gd_tttt`.`test` trx id 67447844 lock_mode X locks rec but not gap waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 32

因为事务
22步占用了X锁,故事务1进行等待。Record lock行锁,非间隙锁。
所以,事务2请求申请S锁需要等待事务1请求并释放X锁,但是X锁被事务2占用,从而导致相互请求造成死锁。以下为每一步的分析。
步骤
事务1
事务2
1

begin
2

delete from test where number = 2;
执行成功,事务2占有number=2下的X锁,类型为记录锁。
3
begin

4
delete from test where number = 2; (事务1希望申请number=2下的X锁,但是由于事务2已经申请了一把X锁,两把X锁互斥,所以X锁申请进入锁请求队列。)

5
出现死锁,事务1权重较小,所以被选择回滚(成为牺牲品)。
insert into test (id, number) values (10, 2);
 由于number字段建立了唯一索引,所以需要申请S锁以便检查duplicate key,由于插入的number的值还是2,所以排在X锁后面。但是前面的X锁的申请只有在事务2 commit或者rollback之后才能成功,此时形成了循环等待,死锁产生。

6

其它一些情况



偶尔会遇到死锁问题,虽然遇到概率不大,但每次遇到要想彻底弄懂其原理并找到解决方案却并不容易。下面收集了一些常见的 MySQL 死锁案例,并对其进行分类汇总,试图通过死锁日志分析出每种死锁的原因,还原出死锁现场。实际上,我们在定位死锁问题时,不仅应该对死锁日志进行分析,还应该结合具体的业务代码,理出每个事务执行的 SQL 语句。
事务一语句
事务二语句
事务一等待锁
事务二等待锁
事务二持有锁
insert
insert
lock_mode X insert intention
lock_mode X insert intention
lock_mode X
insert
insert
lock_mode X locks gap before rec insert intention
lock_mode X locks gap before rec insert intention
lock_mode X locks gap before rec
insert
insert
lock_mode X insert intention
lock_mode X insert intention
lock_mode S
insert
insert
lock mode S
lock_mode X locks gap before rec insert intention
lock_mode X locks rec but not gap
delete
insert
lock_mode X locks rec but not gap
lock mode S
lock_mode X locks rec but not gap
delete
delete
lock_mode X
lock mode S
lock_mode X locks rec but not gap
delete
delete
lock_mode X
lock mode X
lock_mode X locks rec but not gap
delete
delete
lock_mode X locks rec but not gap
lock_mode X
lock_mode X
delete
delete
lock_mode X locks rec but not gap
lock mode X
lock_mode X locks rec but not gap
delete
delete
lock_mode X locks rec but not gap
lock_mode X locks rec but not gap
lock_mode X locks rec but not gap
delete
insert
lock_mode X
lock_mode X locks gap before rec insert intention
lock_mode X locks rec but not gap
delete
insert
lock_mode X
lock_mode X locks gap before rec insert intention
lock_mode S
delete
insert
lock_mode X
lock_mode X locks gap before rec insert intention
lock_mode X
delete
insert
lock_mode X
lock mode S
lock_mode X locks rec but not gap
update
update
lock_mode X locks rec but not gap
lock mode S
lock_mode X locks rec but not gap
update
update
lock_mode X
lock_mode X locks gap before rec insert intention
lock_mode X locks rec but not gap
update
update
lock_mode X locks gap before rec insert intention
lock_mode X locks gap before rec insert intention
lock_mode X
update
delete
lock_mode X locks rec but not gap
lock_mode X
lock mode S
update
update
lock_mode X locks rec but not gap waiting
lock_mode X locks rec but not gap waiting
lock_mode X locks rec but not gap

将在下一期具体分析各种死锁的发生情况。




END






将下一期具体分析各种死锁的发生情
MySQL 锁简介及死锁问题分析
MySQL 锁简介及死锁问题分析

关注更多精彩

况。

喜欢这篇文章就给我个在看吧

以上是关于MySQL 锁简介及死锁问题分析的主要内容,如果未能解决你的问题,请参考以下文章

MySQL锁等待与死锁问题分析

MySQL锁等待与死锁问题分析

万字长文实战分析MySQL死锁

MySQL死锁—加锁处理分析

MySQL - 死锁的产生及解决方案

一个最不可思议的MySQL死锁分析