目录 一、前言 二、mysql之mvcc 2.1 什么是mvcc 2.2 mvcc组成 2.2.1 Undo log 多版本链 2.2.2 ReadView 2.2.3 快照读与当前读 三、RR级别下的事务问题 3.1 RR隔离级别解决的问
目录
锁是Mysql提供的一种保证不同事务读写隔离的重要措施,通过锁机制可以有效提升决多线程下并发处理事务能力。mysql根据使用场景不同,对锁的分类有很多种,比如按照锁的粒度可以分为表锁与行锁,按照锁状态可分为共享锁与排他锁,按模式可分为乐观锁与悲观锁等。不同的锁划分对应着不同的使用场景,同时锁的使用也与mysql的事务隔离机制息息相关,本文来深入探讨一下mysql的另一种容易被忽视的锁,即间隙锁,以及与之相关的相关问题。
在正式开始聊间隙锁之前,还需要了解下mysql的mvcc机制,因为间隙锁的由来与mysql的事务关系密切,同时事务的底层控制是由mysql的mvcc机制来保障。循着这个思路,我们逐渐拨开迷雾,步步为营向前进。
mvc全称多版本并发控制,MVCC 是通过数据行的多个版本管理来实现数据库的并发控制。
通过这项技术,使得在InnoDB的事务隔离级别下执行 一致性读操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的数据行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。
mvcc的实现主要依赖下面的3个主要逻辑实现,分别是:
MVCC核心就是 Undo log多版本链 + Read view,“MV”就是通过 Undo log来保存数据的历史版本,实现多版本的管理。“CC”是通过 Read-view来实现管理,通过 Read-view原则来决定数据是否显示。同时针对不同的隔离级别, Read view的生成策略不同,也就实现了不同的隔离级别。
undo log 也成为回滚日志,用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚 ( 保证事务的原子性 ) 和 MVCC(多版本并发控制 ) 。
举例来说,某一次使用update语句修改一条id为1的数据,如果事务提交失败,那么就需要回滚数据,mysql引擎怎么知道回滚到哪里呢?那就要借助undo log了,undolog中记录了修改之前的数据,所以就可以用于事务回滚。
对于每次操作一条数据的事务来说,每条数据都有两个隐藏字段:
如下图所示,是关于mysql事务操作时对应的undo log版本链的示意图,记录了多个事务对同一条数据发生修改时undo log的情况;

从上图不难看出,每条数据都可能存在多个版本,不同版本之间,通过undo log链条进行连接,通过这种设计,可保证每个事务提交时,一旦需要回滚,能保证同一个事务只能读取到比当前版本更早提交的值,而不能看到更晚提交的值。
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 实现,区别在于:
快照读
快照读又叫一致性读,读取的是快照数据。不加锁简单的 SELECT 都属于快照读,即不加锁的非阻塞读,比如这样:SELECT * FROM user WHERE ...
之所以出现快照读,是基于提高并发性能考虑,快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。
当前读
读取的是记录最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的 SELECT,或者对数据进行增删改都会进行当前读。比如:
SELECT * FROM student LOCK IN SHARE MODE; # 共享锁
SELECT * FROM student FOR UPDATE; # 排他锁
RR即可重复读,即一个事务执行过程中看到的数据,总是跟这个事务在第一次执行时看到的数据是一致的。在学习mysql的事务隔离级别以及各隔离级别所能解决的问题时,是否还记得在这种隔离级别下能够解决什么问题?以及仍存在什么问呢?
下面这张表,详细列举了各事务隔离级别下能够解决的问题,以及未能解决的问题,对照RR隔离级别来说,默认情况下,RR级别可以解决脏读和不可重复读问题,但是仍未解决幻读问题。

简单来说,幻读是指当用户读取某一范围的数据行时,另一个事务又在该范围插入了新行,当用户在读取该范围的数据时会发现有新的幻影行。
注意,在可重复读隔离级别时,默认情况下,普通的查询是快照读(后面的查询一直用的是初次保存的快照数据),因此是不会看到别的事务插入的数据的。因此, 幻读在“当前读”下才会出现(查询语句添加for update,表示当前读),很多人在这里容易糊涂,也是容易混淆一刀切的地方(经常会有面试官问:RR隔离级别下,一定会出现幻读问题吗?所以需要区分是快照读还是当前读,后面会通过案例演示说明);
MVCC多版本并发控制中,读操作可以分为两类: 快照读(Snapshot Read)与当前读 (Current Read)。上述对快照读和当前读有过介绍,它们解决的问题主要如下:
快照读
快照读可以使普通的SELECT 读取数据时不用对表数据进行加锁,从而解决了因为对数据库表的加锁而导致的两个如下问题:
1)解决因加锁导致的修改数据时无法对数据读取问题;
2)解决因加锁导致读取数据时无法对数据进行修改的问题
当前读
当前读是读取的数据库最新的数据,当前读和快照读不同,因为要读取最新的数据而且要保证事务的隔离性,所以当前读是需要对数据进行加锁的(
插入/更新/删除操作,属于当前读,需要加锁,select for update为当前读)
下面演示基于读已提交事务隔离级别下的幻读效果演示
创建如下表,并插入几条数据;
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 |
|---|---|---|
| 1 | begin; | |
| 2 | select * from test where x=3 for update; | |
| 3 | insert into test values(19,3); | |
| 4 | select * from test where x=3 for update; | |
| 5 | commit; |
检查当前数据库事务隔离级别,默认情况下,事务隔离级别为可重复读;
SELECT @@tx_isolation; 
为了模拟幻读效果,先手动调整一下会话的事务隔离级别,使用下面的命令调整
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; 设置完成后,再次查询时,看到事务隔离级别就变成了读已提交;

在第一个mysql的session会话窗口执行如下命令
begin;select * from test where x=3 for update; 
此时在第二个会话窗口insert一条数据

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

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

完整的操作步骤和顺序如下表
| 顺序 | 事务1 | 事务2 |
|---|---|---|
| 1 | begin; | |
| 2 | select * from test where id>1; | begin; |
| 3 | insert into test values(20,3); | |
| 4 | commit; | |
| 5 | select * from test where id>1; | |
| 6 | commit; |
仍然使用上面的表,在开始之前,先将事务隔离级别调整为可重复读;
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步相同,避免了幻读。
像select lock in share mode(共享锁), select for update ; update, insert ,delete这些操作都是一种当前读,读取的是记录的最新版本。在当前读情况下是通过next-key lock(间隙锁)来避免幻读,即加锁阻塞其他事务的当前读。
操作步骤如下:
| 顺序 | 事务A | 事务B |
|---|---|---|
| 1 | begin; | |
| 2 | select * from test where id>1 for update; | begin; |
| 3 | insert into test values(20,3); |
第一个会话事务执行如下操作
begin;select * from test where id>1 for update; 
第二个会话事务开启事务,insert一条数据
begin;insert into test values(20,3); 
通过上面的现象可以看到,第二个会话事务将会阻塞而不能插入成功;
事务A在第2步执行了select for update当前读,会对id>1的数据行记录加锁,同时对(2,+∞)这个区间加间隙锁,两个都是排它锁,会阻塞其他事务的当前读,所以在第2个事务insert新数据时阻塞,从而避免了当前读情况下的幻读。
mysql默认的事务隔离级(可重复读)下可解决大多数场景下的幻读问题,但某些场景下仍然无法完全解决,看下面的这个操作;
| 顺序 | 事务A | 事务B |
|---|---|---|
| 1 | begin; | |
| 2 | select * from test where id>1; | begin; |
| 3 | insert into test values(21,3); | |
| 4 | commit; | |
| 5 | select * from test where id>1 for update; | |
| 6 | commit; |
有兴趣的同学可以按照这个步骤操作一下看下效果,针对上面的操作来做一下分析:
第5步的时候使用了for update,即使用的是当前读,不会再读取Read View,而读取的是当前最新的数据,所以读出了事务B插入的数据。
结合上面的分析结果,做最后如下小结
事务隔离级别是mysql中非常重要的一个点,同时其底层原理也是许多开发者不太好理解的地方,尤其是当事务与锁结合在一起的时候更是容易让人混乱,不管是面试,还是想深入搞清楚原理,或者是排查生产故障问题,搞清不同事务隔离级别以及所能解决的问题,具有很重要的意义。本篇到此结束,感谢观看。
来源地址:https://blog.csdn.net/zhangcongyi420/article/details/132415844
--结束END--
本文标题: mysql 间隙锁原理深度详解
本文链接: https://www.lsjlt.com/news/385656.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-10-23
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
2024-10-22
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0