Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > mysql间隙锁原理

深入探究mysql间隙锁的原理

作者:小码农叔叔

锁是mysql提供的一种保证不同事务读写隔离的重要措施,通过锁机制可以有效提升决多线程下并发处理事务能力,不同的锁划分对应着不同的使用场景,本文来深入探讨一下mysql的另一种容易被忽视的锁,即间隙锁,以及与之相关的相关问题,需要的朋友可以参考下

一、前言

锁是mysql提供的一种保证不同事务读写隔离的重要措施,通过锁机制可以有效提升决多线程下并发处理事务能力。mysql根据使用场景不同,对锁的分类有很多种,比如按照锁的粒度可以分为表锁与行锁,按照锁状态可分为共享锁与排他锁,按模式可分为乐观锁与悲观锁等。不同的锁划分对应着不同的使用场景,同时锁的使用也与mysql的事务隔离机制息息相关,本文来深入探讨一下mysql的另一种容易被忽视的锁,即间隙锁,以及与之相关的相关问题。

二、mysql之mvcc

在正式开始聊间隙锁之前,还需要了解下mysql的mvcc机制,因为间隙锁的由来与mysql的事务关系密切,同时事务的底层控制是由mysql的mvcc机制来保障。循着这个思路,我们逐渐拨开迷雾,步步为营向前进。

2.1 什么是mvcc

mvc全称多版本并发控制,MVCC 是通过数据行的多个版本管理来实现数据库的并发控制。

通过这项技术,使得在InnoDB的事务隔离级别下执行 一致性读操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的数据行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。

2.2 mvcc组成

mvcc的实现主要依赖下面的3个主要逻辑实现,分别是:

MVCC核心就是 Undo log多版本链 + Read view,“MV”就是通过 Undo log来保存数据的历史版本,实现多版本的管理。“CC”是通过 Read-view来实现管理,通过 Read-view原则来决定数据是否显示。同时针对不同的隔离级别, Read view的生成策略不同,也就实现了不同的隔离级别。

2.2.1 Undo log 多版本链

undo log 也成为回滚日志,用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚 ( 保证事务的原子性 ) 和 MVCC(多版本并发控制 ) 。

举例来说,某一次使用update语句修改一条id为1的数据,如果事务提交失败,那么就需要回滚数据,mysql引擎怎么知道回滚到哪里呢?那就要借助undo log了,undolog中记录了修改之前的数据,所以就可以用于事务回滚。

对于每次操作一条数据的事务来说,每条数据都有两个隐藏字段:

如下图所示,是关于mysql事务操作时对应的undo log版本链的示意图,记录了多个事务对同一条数据发生修改时undo log的情况;

从上图不难看出,每条数据都可能存在多个版本,不同版本之间,通过undo log链条进行连接,通过这种设计,可保证每个事务提交时,一旦需要回滚,能保证同一个事务只能读取到比当前版本更早提交的值,而不能看到更晚提交的值。

2.2.2 ReadView

Read View是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。

Read View中比较重要的字段有4个:

如下图,记录了Read View中当前事务发生状态时相关的几个字段信息,对照上面的几个字段的解释可以进一步理解,举例来说,某个事务第一次执行查询,生成了一致性视图read-view,里面保存了当前事务相关的信息,再次查询时就会从undo log 中拿最新的一条记录开始跟 read-view 做对比,如果不符合比较规则,就根据回滚指针回滚到上一条记录继续比较,直到得到符合比较条件的查询结果。

Read View如何判断记录的某个版本可见呢?规则大致如下:

1)如果当前记录的事务id落在绿色部分(trx_id < min_id),表示这个版本是已提交的事务生成的,可读;

2)如果当前记录的事务id落在红色部分(trx_id > max_id),表示这个版本是由将来启动的事务生成的,不可读;

3)如果当前记录的事务id落在黄色部分(min_id <= trx_id <= max_id),则又可以分为两种情况:

  • 若当前记录的事务id在未提交事务的数组中,则此条记录不可读;
  • 若当前记录的事务id不在未提交事务的数组中,则此条记录可读;

在mysql的事务隔离级别中,RC(读已提交) 和 RR(可重复读) 隔离级别都是基于 MVCC 实现,区别在于:

2.2.3 快照读与当前读

快照读

快照读又叫一致性读,读取的是快照数据。不加锁简单的 SELECT 都属于快照读,即不加锁的非阻塞读,比如这样:SELECT * FROM user WHERE ...

之所以出现快照读,是基于提高并发性能考虑,快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。

当前读

读取的是记录最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的 SELECT,或者对数据进行增删改都会进行当前读。比如:

SELECT * FROM student LOCK IN SHARE MODE; # 共享锁

SELECT * FROM student FOR UPDATE; # 排他锁

三、RR级别下的事务问题

RR即可重复读,即一个事务执行过程中看到的数据,总是跟这个事务在第一次执行时看到的数据是一致的。在学习mysql的事务隔离级别以及各隔离级别所能解决的问题时,是否还记得在这种隔离级别下能够解决什么问题?以及仍存在什么问呢?

3.1 RR隔离级别解决的问题

下面这张表,详细列举了各事务隔离级别下能够解决的问题,以及未能解决的问题,对照RR隔离级别来说,默认情况下,RR级别可以解决脏读和不可重复读问题,但是仍未解决幻读问题。

3.1.1 幻读问题

简单来说,幻读是指当用户读取某一范围的数据行时,另一个事务又在该范围插入了新行,当用户在读取该范围的数据时会发现有新的幻影行。

注意,在可重复读隔离级别时,默认情况下,普通的查询是快照读(后面的查询一直用的是初次保存的快照数据),因此是不会看到别的事务插入的数据的。因此, 幻读在“当前读”下才会出现(查询语句添加for update,表示当前读),很多人在这里容易糊涂,也是容易混淆一刀切的地方(经常会有面试官问:RR隔离级别下,一定会出现幻读问题吗?所以需要区分是快照读还是当前读,后面会通过案例演示说明);

MVCC多版本并发控制中,读操作可以分为两类: 快照读( Snapshot Read )与当前读 ( Current Read )。上述对快照读和当前读有过介绍,它们解决的问题主要如下:

快照读

快照读可以使普通的SELECT 读取数据时不用对表数据进行加锁,从而解决了因为对数据库表的加锁而导致的两个如下问题:

1)解决因加锁导致的修改数据时无法对数据读取问题;

2)解决因加锁导致读取数据时无法对数据进行修改的问题

当前读

当前读是读取的数据库最新的数据,当前读和快照读不同,因为要读取最新的数据而且要保证事务的隔离性,所以当前读是需要对数据进行加锁的( 插入/更新/删除操作,属于当前读,需要加锁 , select for update 为当前读)

3.2 幻读效果演示

下面演示基于读已提交事务隔离级别下的幻读效果演示

3.2.1 准备测试表和数据

创建如下表,并插入几条数据;

CREATE TABLE `test` (
  `id` int(12) NOT NULL,
  `x` int(12) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into test values(1,3);
insert into test values(2,3);
insert into test values(3,3);
insert into test values(5,3);
insert into test values(17,3);

完整操作步骤

顺序事务A事务B
1begin;
2select * from test where x=3 for update;
3insert into test values(19,3);
4select * from test where x=3 for update;
5commit;

3.2.2 修改事务级别

检查当前数据库事务隔离级别,默认情况下,事务隔离级别为可重复读;

SELECT @@tx_isolation;

为了模拟幻读效果,先手动调整一下会话的事务隔离级别,使用下面的命令调整

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

设置完成后,再次查询时,看到事务隔离级别就变成了读已提交;

3.2.3 开启两个session会话并执行事务操作

在第一个mysql的session会话窗口执行如下命令

begin;
select * from test where x=3 for update;

此时在第二个会话窗口insert一条数据

再在第一个会话窗口查询x=3的数据,检查数据,发现能够查询到上面插入的这条数据;

3.3 间隙锁解决幻读问题

3.3.1 间隙锁概述

幻读是如何产生的呢?产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了 next-key 锁,就是记录锁和间隙锁的组合。

可以对照下面这张图深入理解上面几种锁的含义

3.3.2 基于快照读解决幻读问题

完整的操作步骤和顺序如下表

顺序事务1事务2
1begin;
2select * from test where id>1;begin;
3insert into test values(20,3);
4commit;
5select * from test where id>1;
6commit;

仍然使用上面的表,在开始之前,先将事务隔离级别调整为可重复读;

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;SELECT @@tx_isolation;

开启第一个会话,查询id>1的数据

begin;
select * from test where id > 1;

开启第二个会话并插入一条数据

begin;
insert into test values(20,3);
commit;

第一个会话再次查询id>1的数据,可以发现第二个会话插入的数据在当前的会话事务中并没有查到;

提交第一个会话的事务,再次查询,此时就能查到数据了

总结:

可重复读隔离级别下是通过MVCC来避免幻读的,具体的实现方式在事务开启后的第一条select语句生成一张Read View(数据库系统当前的一个快照),之后的每一次快照读都会读取这个Read View。

在上面的操作流程中,在第2步生成一张Read View,所以在第5步时读取到数据和第2步相同,避免了幻读。

3.3.3 当前读基于间隙锁解决幻读问题

select lock in share mode(共享锁), select for update ; update, insert ,delete这些操作都是一种当前读,读取的是记录的最新版本。在当前读情况下是通过next-key lock(间隙锁)来避免幻读,即加锁阻塞其他事务的当前读。

操作步骤如下:

顺序事务A事务B
1begin;
2select * from test where id>1 for update;begin;
3insert into test values(20,3);

第一个会话事务执行如下操作

begin;
select * from test where id>1 for update;

第二个会话事务开启事务,insert一条数据

begin;
select * from test where id>1 for update;

通过上面的现象可以看到,第二个会话事务将会阻塞而不能插入成功;

事务A在第2步执行了select for update当前读,会对id>1的数据行记录加锁,同时对(2,+∞)这个区间加间隙锁,两个都是排它锁,会阻塞其他事务的当前读,所以在第2个事务insert新数据时阻塞,从而避免了当前读情况下的幻读。

3.4 可重复读一定解决了幻读问题吗

mysql默认的事务隔离级(可重复读)下可解决大多数场景下的幻读问题,但某些场景下仍然无法完全解决,看下面的这个操作;

顺序事务A事务B
1begin;
2select * from test where id>1;begin;
3insert into test values(21,3);
4commit;
5select * from test where id>1 for update;
6commit;

有兴趣的同学可以按照这个步骤操作一下看下效果,针对上面的操作来做一下分析:

3.4.1 原因分析

第5步的时候使用了for update,即使用的是当前读,不会再读取Read View,而读取的是当前最新的数据,所以读出了事务B插入的数据。

3.4.2 总结

结合上面的分析结果,做最后如下小结

四、写在文末

事务隔离级别是mysql中非常重要的一个点,同时其底层原理也是许多开发者不太好理解的地方,尤其是当事务与锁结合在一起的时候更是容易让人混乱,不管是面试,还是想深入搞清楚原理,或者是排查生产故障问题,搞清不同事务隔离级别以及所能解决的问题,具有很重要的意义。本篇到此结束,感谢观看。

以上就是深入探究mysql间隙锁原理的详细内容,更多关于mysql间隙锁原理的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文