两万字详解!InnoDB锁专题!

前言大家好,我是捡田螺的小男孩。本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。

两万字详解!InnoDB锁专题!插图亿华云

一、为什么需要加锁?

数据库为什么需要加锁呢?

在日常生活中,如果你心情不好。想要一个人静静,不想被比别人打扰,你就可以把自己关进房间里,并且反锁。

同理,对于MySQL数据库来说的话,一般的对象都是一个事务一个事务来说的。所以,如果一个事务内,正在写某个SQL,我们肯定不想它被别的事务影响到嘛?因此,数据库设计大叔,就给被操作的SQL加上锁。

专业一点的说法:
如果有多个并发请求存取数据,在数据就可能会产生多个事务同时操作同一行数据。如果并发操作不加控制,不加锁的话,就可能写入了不正确的数据,或者导致读取了不正确的数据,破坏了数据的一致性。因此需要考虑加锁。

1. 事务并发存在的问题脏读:一个事务A读取到事务B未提交的数据,就是脏读。不可重复读:事务A被事务B干扰到了!在事务A范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。幻读:事务A查询一个范围的结果集,另一个并发事务B往这个范围中插入/删除了数据,并静悄悄地提交,然后事务A再次查询相同的范围,两次读取得到的结果集不一样了,这就是幻读。2. 一个加锁和不加锁对比的例子

我们知道MySQL数据库有四大隔离级别读已提交(RC)、可重复读(RR)、串行化、读未提交。如果是读未提交隔离级别,并发情况下,它是不加锁的,因此就会存在脏读、不可重复读、幻读的问题。

为了更通俗易懂一点,还是给大家举个例子吧,虽然东西挺简单的。假设现在有表结构和数据如下:

CREATE TABLE `account` (

`id` int(11) NOT NULL,

`name` varchar(255) DEFAULT NULL,

`balance` int(11) DEFAULT NULL,

PRIMARY KEY (`id`),

UNIQUE KEY `un_name_idx` (`name`) USING BTREE

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into account(id,name,balance)values (1,Jay,100);

insert into account(id,name,balance)values (2,Eason,100);

insert into account(id,name,balance)values (3,Lin,100);

在READ-UNCOMMITTED(读未提交) 隔离级别下,假设现在有两个事务A、B:

假设现在Jay的余额是100,事务A正在准备查询Jay的余额这时候,事务B先扣减Jay的余额,扣了10最后A 读到的是扣减后的余额

两万字详解!InnoDB锁专题!插图1亿华云两万字详解!InnoDB锁专题!插图2亿华云

手动验证了一把,流程如下:

两万字详解!InnoDB锁专题!插图3亿华云两万字详解!InnoDB锁专题!插图4亿华云

由上图可以发现,事务A、B交替执行,事务A被事务B干扰到了,因为事务A读取到事务B未提交的数据,这就是脏读。为什么存在脏读问题呢?这是因为在读未提交的隔离级别下执行写操作,并没有对SQL加锁,因此产生了脏读这个问题。

我们再来看下,在串行化隔离级别下,同样的SQL执行流程,又是怎样的呢?

两万字详解!InnoDB锁专题!插图5亿华云两万字详解!InnoDB锁专题!插图6亿华云

为啥会阻塞等待超时呢?这是因为串行化隔离级别下,对写的SQL加锁啦。我们可以再看下加了什么锁,命令如下:

SET GLOBAL innodb_status_output=ON; -- 开启输出

SET GLOBAL innodb_status_output_locks=ON; -- 开启锁信息输出

SHOW ENGINE INNODB STATUS

锁相关的输出内容如下:
两万字详解!InnoDB锁专题!插图7亿华云

两万字详解!InnoDB锁专题!插图8亿华云

我们可以看到了这么一把锁:lock_mode X locks rec but not
gap,它到底是一种什么锁呢?来来来,我们一起来学习下InnoDB的七种锁。

二、InnoDB的七种锁介绍

两万字详解!InnoDB锁专题!插图9亿华云

1. 共享/排他锁

InnoDB呢实现了两种标准的行级锁:共享锁(简称S锁)、排他锁(简称X锁)。

共享锁:简称为S锁,在事务要读取一条记录时,需要先获取该记录的S锁。排他锁:简称X锁,在事务需要改动一条记录时,需要先获取该记录的X锁。如果事务T1持有行R的S锁,那么另一个事务T2请求访问这条记录时,会做如下处理:T2 请求S锁立即被允许,结果 T1和T2都持有R行的S锁T2 请求X锁不能被立即允许,此操作会阻塞

如果T1持有行R的X锁,那么T2请求R的X、S锁都不能被立即允许,T2 必须等待T1释放X锁才可以,因为X锁与任何的锁都不兼容。

S锁和X锁的兼容关系如下图表格:

两万字详解!InnoDB锁专题!插图10亿华云两万字详解!InnoDB锁专题!插图11亿华云

X锁和S锁是对于行记录来说的话,因此可以称它们为行级锁或者行锁。我们认为行锁的粒度就比较细,其实一个事务也可以在表级别下加锁,对应的,我们称之为表锁。给表加的锁,也是可以分为X锁和S锁的哈。

如果一个事务给表已经加了S锁,则:

别的事务可以继续获得该表的S锁,也可以获得该表中某些记录的S锁。别的事务不可以继续获得该表的X锁,也不可以获得该表中某些记录的X锁。

如果一个事务给表加了X锁,那么:

别的事务不可以获得该表的S锁,也不可以获得该表某些记录的S锁。别的事务不可以获得该表的X锁,也不可以继续获得该表某些记录的X锁。2. 意向锁

什么是意向锁呢?意向锁是一种不与行级锁冲突的表级锁。未来的某个时刻,事务可能要加共享或者排它锁时,先提前声明一个意向。注意一下,意向锁,是一个表级别的锁哈。

为什么需要意向锁呢?或者换个通俗的说法,为什么要加共享锁或排他锁时的时候,需要提前声明个意向锁呢呢?

因为InnoDB是支持表锁和行锁共存的,如果一个事务A获取到某一行的排他锁,并未提交,这时候事务B请求获取同一个表的表共享锁。因为共享锁和排他锁是互斥的,因此事务B想对这个表加共享锁时,需要保证没有其他事务持有这个表的表排他锁,同时还要保证没有其他事务持有表中任意一行的排他锁。

然后问题来了,你要保证没有其他事务持有表中任意一行的排他锁的话,去遍历每一行?这样显然是一个效率很差的做法。为了解决这个问题,InnoDB的设计大叔提出了意向锁。

意向锁是如何解决这个问题的呢? 我们来看下。

意向锁分为两类:

意向共享锁:简称IS锁,当事务准备在某些记录上加S锁时,需要现在表级别加一个IS锁。意向排他锁:简称IX锁,当事务准备在某条记录上加上X锁时,需要现在表级别加一个IX锁。

比如:

select ... lock in share mode,要给表设置IS锁;select ... for update,要给表设置IX锁;

意向锁又是如何解决这个效率低的问题呢:

如果一个事务A获取到某一行的排他锁,并未提交,这时候表上就有意向排他锁和这一行的排他锁。这时候事务B想要获取这个表的共享锁,此时因为检测到事务A持有了表的意向排他锁,因此事务A必然持有某些行的排他锁,也就是说事务B对表的加锁请求需要阻塞等待,不再需要去检测表的每一行数据是否存在排他锁啦。这样效率就高很多啦。

意向锁仅仅表明意向的锁,意向锁之间并不会互斥,是可以并行的,整体兼容性如下图所示:

两万字详解!InnoDB锁专题!插图12亿华云两万字详解!InnoDB锁专题!插图13亿华云

3. 记录锁(Record Lock)

记录锁是最简单的行锁,仅仅锁住一行。如:SELECT c1 FROM t WHERE c1 = 10 FOR
UPDATE,如果c1字段是主键或者是唯一索引的话,这个SQL会加一个记录锁(Record Lock)

记录锁永远都是加在索引上的,即使一个表没有索引,InnoDB也会隐式的创建一个索引,并使用这个索引实施记录锁。它会阻塞其他事务对这行记录的插入、更新、删除。

一般我们看死锁日志时,都是找关键词,比如lock_mode X locks rec but not
gap),就表示一个X型的记录锁。记录锁的关键词就是rec but not gap。以下就是一个记录锁的日志:

RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`

trx id 10078 lock_mode X locks rec but not gap

Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0

0: len 4; hex 8000000a; asc ;;

1: len 6; hex 00000000274f; asc O;;

2: len 7; hex b60000019d0110; asc ;;

4. 间隙锁(Gap Lock)

为了解决幻读问题,InnoDB引入了间隙锁(Gap
Lock)。间隙锁是一种加在两个索引之间的锁,或者加在第一个索引之前,或最后一个索引之后的间隙。它锁住的是一个区间,而不仅仅是这个区间中的每一条数据。

比如lock_mode X locks gap before rec表示X型gap锁。以下就是一个间隙锁的日志:

RECORD LOCKS space id 177 page no 4 n bits 80 index idx_name of table `test2`.`account`

trx id 38049 lock_mode X locks gap before rec

Record lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0

0: len 3; hex 576569; asc Wei;;

1: len 4; hex 80000002; asc ;;

5. 临键锁(Next-Key Lock)

Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。说得更具体一点就是:临键锁会封锁索引记录本身,以及索引记录之前的区间,即它的锁区间是前开后闭,比如(5,10]。

如果一个会话占有了索引记录R的共享/排他锁,其他会话不能立刻在R之前的区间插入新的索引记录。官网是这么描述的:

If one session has a shared or exclusive lock on record R in an index,
another session cannot insert a new index record in the gap immediately before R
in the index order.

6. 插入意向锁

插入意向锁,是插入一行记录操作之前设置的一种间隙锁。这个锁释放了一种插入方式的信号。它解决的问题是:多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,就不会阻塞彼此。

假设有索引值4、7,几个不同的事务准备插入5、6,每个锁都在获得插入行的独占锁之前用插入意向锁各自锁住了4、7之间的间隙,但是不阻塞对方因为插入行不冲突。以下就是一个插入意向锁的日志:

RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child`

trx id 8731 lock_mode X locks gap before rec insert intention waiting

Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0

0: len 4; hex 80000066; asc f;;

1: len 6; hex 000000002215; asc " ;;

2: len 7; hex 9000000172011c; asc r ;;...

锁模式兼容矩阵(横向是已持有锁,纵向是正在请求的锁)如下:

两万字详解!InnoDB锁专题!插图14亿华云两万字详解!InnoDB锁专题!插图15亿华云

7. 自增锁

自增锁是一种特殊的表级别锁。它是专门针对AUTO_INCREMENT类型的列,对于这种列,如果表中新增数据时就会去持有自增锁。简言之,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。

官方文档是这么描述的:

An AUTO-INC lock is a special table-level lock taken by transactions
inserting into tables with AUTO_INCREMENT columns. In the simplest case, if one
transaction is inserting values into the table, any other transactions must wait
to do their own inserts into that table, so that rows inserted by the first
transaction receive consecutive primary key values.

假设有表结构以及自增模式是1,如下:

mysql

THE END
Copyright © 2024 亿华云