[date: 2018-08-24 17:57] [visits: 26]

InnoDB Gap锁

这篇文章基于大家对于数据库事务隔离级别以及数据库锁有一定的认识,然后讲一讲InnoDB的Gap锁。

事务隔离级别

数据库的事务隔离级别主要用于定义并发事务之间的相互影响,比如如A事务将一条数据的id由2修改为3但未提交,这时B事务查询该条数据,id是2还是3?事务A提交后呢?

造成这种差异的原因就是事务隔离级别,具体分为以下几种:

可能读取到其他会话中未提交事务修改的数据,这种现象称为脏读

只能读取到已经其他事务提交过的数据,但在同一个事务中的相同查询结果可能不一致(其他事务update、delete)

可重复读,在同一个事务内的查询与事务开始时刻是一致的,但是可能出现幻读:“同一事务中的相同查询得到前一次不存在的记录行(其他事务insert)”

串行化的读写,读写相互阻塞

InnoDB的事务隔离级别默认是RR-可重复读,理论上可能出现幻读,但真实情况是InnoDB的RR事务隔离级别并不会出现幻读,原因即是InnoDB内部的Next-Key锁。

Next-Key锁

Next-Key锁由行锁 + Gap锁组成。

行锁

当需要对表中的某条数据进行写操作(insert、update、delete、select for update)时,需要先获取记录的排他锁(X锁),这个就称为行锁。

create table x(`id` int, `num` int, index `idx_id` (`id`));
insert into x values(1, 1), (2, 2);

-- 事务A
update x set id = 1 where id = 1;

-- 事务B
-- 如果事务A没有commit,id=1的记录拿不到X锁,将出现等待
update x set id = 1 where id = 1;

-- 事务C
-- id=2的记录可以拿到X锁,不会出现等待
update x set id = 2 where id = 2;

针对InnoDB RR隔离级别,上述SQL示例展示了行锁的特点:“锁定特定行不允许进行修改”,但行锁是基于表索引的,如果where条件中用的是num字段(非索引列)将产生不一样的现象:

-- 事务A
update x set num = 1 where num = 1;

-- 事务B
-- 由于事务A中num字段上没有索引将产生表锁,导致整张表的写操作都会出现等待
update x set num = 1 where num = 1;

-- 事务C
-- 同理,会出现等待
update x set num = 2 where num = 2;

-- 事务D
-- 等待
insert into x values(3, 3);

Gap锁

在MySQL中select称为快照读,不需要锁,而insert、update、delete与select for update则称为当前读,需要给数据加锁,幻读中的“读”即是针对当前读。

RR事务隔离级别允许存在幻读,但InnoDB RR级别却通过Gap锁避免了幻读,用SQL示例解释一下现象:

create table x(`id` int, `num` int, index `idx_id` (`id`));
insert into x values(1, 1), (5, 5), (10, 10);

-- 下面使用前缀表明SQL所属事务
A: begin
B: begin
-- 下面的SQL查询id=5的记录,for update会为记录添加行锁
A: select * from x where id = 5 for update; 
B: insert into x values(4, 4);
B: commit;
-- 事务B提交了一条id=4的数据,理论上下面的SQL能够查询到1、4、5与10四条数据,但在InnoDB中的Gap锁会导致B事务insert进入等待,从而这里只会查到1、5与10三条数据
A: select * from x;

RR级别本身提到的可重复读主要是针对其他事务的update与delete不会对本事务产生影响,而其他事务的insert操作对本事务的查询产生影响则称为幻读,在InnoDB里面并不会出现这种现象。就如上述示例中,B事务由于Gap锁的原因会导致insert进入等待无法成功,A事务自然无法读到insert的数据。

Gap翻译为间隙,指的是索引与索引之间的区间,如上面的x表中id是一个索引列且有三个值:1、5与10,因此存在以下四个间隙:

(-inf, 1)、(1, 5)、(5, 10)、(10, +inf)

当select for update为索引加上锁(行锁)的同时,索引值所处的区间会被加上Gap锁,而被加上Gap锁的区间无法insert数据,拿上一个例子来说,select * from x where id = 5 for update会给id=5的记录添加行锁,同时id=5前后的区间(1, 5)与(5, 10)会被加上Gap锁,因此在锁没被释放之前,id=4的记录无法insert。

除了id=4的记录无法insert外,id属于[2, 3, 4, 6, 7, 8, 9]的记录都无法insert,但针对id=1与id=10的临界情况则需要结合主键进行考虑,参考以下SQL示例:

create table x(`id` int primary key, `num` int, index `idx_num` (`num`));
insert into x values(1, 1), (5, 5), (10, 10);

-- 事务A
select * from x where num = 5 for update;

-- 事务B
-- 由于区间(1, 5)中,num=1的记录主键id=1,而(id=2, num=1)则落在(1, 5)这个区间上,所以insert会进入等待
insert into x values(2, 1); 

-- 事务C
-- (id=0, num=1)落在区间外,这里insert会成功
insert into x values(0, 1); 

通过上述例子,不难看出Gap的计算是通过对索引字段与主键字段两者的order by。

tips:Gap锁针对的是非唯一索引,而如果是唯一索引则不会存在Gap锁

总结

温故而知新,对于MySQL、InnoDB等相关知识,平时用的少,学过的容易忘,这次通过写文章进一步加深对这一块的理解,加油~