前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【眼见为实】自己动手实践理解数据库REPEATABLE READ && Next-Key Lock

【眼见为实】自己动手实践理解数据库REPEATABLE READ && Next-Key Lock

原创
作者头像
撸码那些事
发布于 2018-06-16 13:33:08
发布于 2018-06-16 13:33:08
62600
代码可运行
举报
文章被收录于专栏:撸码那些事撸码那些事
运行总次数:0
代码可运行

[REPEATABLE READ]

首先设置数据库隔离级别为可重复读(REPEATABLE READ):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
set global transaction isolation level REPEATABLE READ ;
set session transaction isolation level REPEATABLE READ ; 

[REPEATABLE READ]能解决的问题之一

[REPEATABLE READ]隔离级别解决了不可重复读的问题,一个事务中多次读取不会出现不同的结果,保证了可重复读。 还是上一篇中模拟不可重复读的例子: 事务1

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
START TRANSACTION;SELECT sleep(5);UPDATE users SET state=1 WHERE id=1;
COMMIT;

事务2

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
START TRANSACTION;SELECT * FROM users WHERE id=1;SELECT sleep(10);SELECT * FROM users WHERE id=1;
COMMIT;  

事务1先于事务2执行。 事务1的执行信息

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[SQL 1]START TRANSACTION;
受影响的行: 0
时间: 0.000s

[SQL 2]
SELECT sleep(5);
受影响的行: 0
时间: 5.001s

[SQL 3]
UPDATE users SET state=1 WHERE id=1;
受影响的行: 1
时间: 0.000s

[SQL 4]
COMMIT;
受影响的行: 0
时间: 0.062s

事务2的执行信息

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[SQL 1]
    SELECT * FROM users WHERE id=1;
受影响的行: 0
时间: 0.000s

[SQL 2]
    SELECT sleep(10);
受影响的行: 0
时间: 10.001s

[SQL 3]
    SELECT * FROM users WHERE id=1;
受影响的行: 0
时间: 0.001s

[SQL 4]
    COMMIT;
受影响的行: 0
时间: 0.001s

执行结果

结论: 可重复读[REPEATABLE READ]隔离级别解决了不可重复读的问题。

分析: 可重复读[REPEATABLE READ]隔离级别能解决不可重复读根本原因其实就是前文讲过的read view的生成机制和[READ COMMITTED]不同。 [READ COMMITTED]:只要是当前语句执行前已经提交的数据都是可见的。 [REPEATABLE READ]:只要是当前事务执行前已经提交的数据都是可见的。 在[REPEATABLE READ]的隔离级别下,创建事务的时候,就生成了当前的global read view,一直维持到事务结束。这样就能实现可重复读。

在模拟不可重复读的事务中,事务2创建时,会生成一份read view。事务1的事务id trx_id1=1,事务2的事务id trx_id2=2。假设事务2第一次读取数据前的此行数据的事务trx_id=0。事务2中语句①执行前生成的read view为{1},trx_id_min=1,trx_id_max=1。因为trx_id(0)<trx_id_min(1),该行记录的当前值可见,将该可见行的值state=0返回。因为在[REPEATABLE READ]隔离级别下,只有在事务创建时才会重新生成read view ,事务2第二次读取数据之前事务1对数据进行了更新操作,此行数据的事务trx_id=1。trx_id_min(1)=trx_id(1)=trx_id_max(1),此时此行数据对事务2是不可见的,从该行记录的DB_ROLL_PTR指针所指向的回滚段中取出最新的undo-log的版本号的数据,将该可见行的值state=0返回。所以事务2第二次读取数据时的处理和第一次读取时是一致的,读取的state=0。数据是可重复读的。

从事务1的执行信息中的[SQL 3]我们可以得知,[REPEATABLE READ]隔离级别读操作也是不加锁的。因为如果读需要加S锁的话,是在事务结束时释放S锁的。那么事务1[SQL 3]进行更新操作申请X锁的时候便会等待事务2的S锁释放。现实并不是。

我们知道,MySql的InnoDB引擎是通过MVCC的方式在保证数据的安全性的同时,实现了读的非阻塞。MVCC模式需要额外的存储空间,需要做更多的行检查工作;但是保证了读操作不用加锁,提升了性能,是一种典型的牺牲空间换取时间思想的实现。需要注意的是,MVCC只在[READ COMMITTED]和[REPEATABLE READ]两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容,因为[READ UNCOMMITTED]总是读取最新的数据行,而不是符合当前事务版本的数据行。而[SERIALIZABLE]则会对所有读取的行都加锁。

通过亲自实践模拟分析[READ COMMITTED]和[REPEATABLE READ]两个隔离级别的工作机制,我们也能深刻的体会到各个数据库引擎实现各种隔离级别的方式并不是和标准sql中的封锁协议定义一一对应的。

[REPEATABLE READ]能解决的问题之二

幻读其实是不可重复读的一种特殊情况。不可重复读是对数据的修改更新产生的;而幻读是插入或删除数据产生的。所谓的幻读有2种情况,一个事物之前读的时候,读到一条记录,再读发现记录没有了,被其它事务删了,另外一种是之前读的时候记录不存在,再读发现又有这条记录,其它事物插入了一条记录。

事务1

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
START TRANSACTION;
SELECT * FROM users;
SELECT sleep(10);
SELECT * FROM users;
COMMIT;

事务2

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
START TRANSACTION;
SELECT sleep(5);
INSERT INTO users VALUES(2,'song',2);
COMMIT;

执行结果

1.预期结果

2.实际结果

事务1中并没有读取到事务2新插入的数据,并没有发生幻读现象。这有点出乎我的意料,难道Mysql[REPEATABLE READ]隔离级别能解决幻读问题?按照封锁协议定义,三级封锁协议是解决不了幻读的问题的。只有最强封锁协议,读和写都对整个表加锁,才能解决幻读的问题。但是这样做相当于所有的操作串行化,数据库支持并发的能力会变得极差。所以Mysql的InnoDB引擎通过自己的方式在[REPEATABLE READ]隔离级别上解决了幻读的问题,下面我们探究一下InnoDB引擎是如何解决幻读问题的。

分析: InnoDB有三种行锁的算法: 1.Record Lock:单个行记录上的锁。 2.Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。 3.Next-Key Lock:1+2,锁定一个范围,并且锁定记录本身。主要目的是解决幻读的问题。

在[REPEATABLE READ]级别下,如果查询条件能使用上唯一索引,或者是一个唯一的查询条件,那么仅加行锁(通过唯一的查询条件查询唯一行,当然不会出现幻读的现象);如果是一个范围查询,那么就会给这个范围加上 Gap锁或者 Next-Key锁 (行锁+Gap锁)。理论上不会发生幻读

验证一下Gap Lock和Next-Key Lock的存在

我们可以通过自己操作来验证一下Gap Lock和Next-Key Lock的存在。 首先我们需要给state字段加上索引。然后准备几条数据,如下图:

事务1

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
START TRANSACTION;SELECT * FROM users WHERE state=3 for UPDATE;

事务2

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[SQL]INSERT INTO users VALUES(5,'song',1);
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

[SQL]INSERT INTO users VALUES(6,'song',2);
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

[SQL]INSERT INTO users VALUES(6,'song',3);
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

[SQL]INSERT INTO users VALUES(6,'song',4);
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

[SQL]INSERT INTO users VALUES(5,'song',0);
受影响的行: 1
时间: 0.120s

[SQL]INSERT INTO users VALUES(6,'song',5);
受影响的行: 1
时间: 0.195s

[SQL]INSERT INTO users VALUES(7,'song',7);
受影响的行: 1
时间: 0.041s

因为InnoDB对于行的查询都是采用了Next-Key Lock的算法,锁定的不是单个值,而是一个范围(GAP)。上面索引值有1,3,5,8,其记录的GAP的区间如下: (-∞,1],(1,3],(3,5],(5,8],(8,+∞)。是一个左开右闭的空间。需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上Gap Lock。事务1语句①锁定的范围是(1,3],下个键值范围是(3,5],所以插入1~4之间的值的时候都会被锁定,要求等待,等待超过一定时间便会进行超时处理(Mysql默认的超时时间为50秒)。插入非这个范围内的值都正常。

[REPEATABLE READ]读到底加不加锁?

当我理解了[REPEATABLE READ]隔离级别是如何解决幻读问题时,随即产生了另一个疑问。[READ COMMITED]和[REPEATABLE READ]通过MVCC的方式避免了读操作加锁的问题,但是[REPEATABLE READ]又为了解决幻读的问题加Gap Lock或Next-Key Lock。那么问题来了,[REPEATABLE READ]读到底加不加锁?我对这个问题是百思不得其解,直到读到了这篇文章才算理解了一些。

我们可以思考一下如果InnoDB对普通的查询也加了锁,那和序列化(SERIALIZABLE)的区别又在哪里呢?我的理解是InnoDB提供了Next-Key Lock,但需要应用自己去加锁。这里又涉及到一致性读(快照读)和当前读。如果我们选择一致性读,也就是MVCC的模式,读就不需要加锁,读到的数据是通过Read View控制的。如果我们选择当前读,读是需要加锁的,也就是Next-Key Lock,其他的写操作需要等待Next-Key Lock释放才可写入,这种方式读取的数据是实时的。

一致性读很好理解,读不加锁,不堵塞读。当前读对读加锁可能比较难理解,我们可以通过一个例子来理解一下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
事务1									   事务2
START TRANSACTION; 						  START TRANSACTION; 
SELECT * FROM users;
										INSERT INTO users VALUES (2, 'swj',2);
                                		   COMMIT;
SELECT * FROM users;
SELECT * FROM users LOCK IN SHARE MODE;
SELECT * FROM users FOR UPDATE;

执行结果

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mysql> SELECT * FROM users;
+----+------+-------+
| id | name | state |
+----+------+-------+
|  1 | swj  |     1 |
+----+------+-------+
1 row in set (0.04 sec)

mysql> SELECT * FROM users;
+----+------+-------+
| id | name | state |
+----+------+-------+
|  1 | swj  |     1 |
+----+------+-------+
1 row in set (0.08 sec)

mysql> SELECT * FROM users LOCK IN SHARE MODE;
+----+------+-------+
| id | name | state |
+----+------+-------+
|  1 | swj  |     1 |
|  2 | swj  |     2 |
+----+------+-------+
2 rows in set (0.00 sec)

mysql> SELECT * FROM users FOR UPDATE;
+----+------+-------+
| id | name | state |
+----+------+-------+
|  1 | swj  |     1 |
|  2 | swj  |     2 |
+----+------+-------+
2 rows in set (0.00 sec)

结论MVCC是实现的是快照读,Next-Key Lock是对当前读。MySQL InnoDB的可重复读并不保证避免幻读,需要应用使用加锁读来保证,而这个加锁读使用到的机制就是Next-Key Lock


作者:撸码那些事

声明:本文为博主学习感悟总结,水平有限,如果不当,欢迎指正。如果您认为还不错,不妨关注一下我的【微信公众号】,第一时间获取文章更新。转载与引用请注明出处。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
【C语言】函数递归总结
上述就是一个简单的递归程序,只不过上面的递归只是为了演示递归的基本形式,不是为了解决问题,代码最终也会陷入死递归导致栈溢出(Stackoverflow)。
用户11290673
2024/09/25
1070
【C语言】函数递归总结
【C语言】函数递归(含扫雷进阶思路)
    递归是学习C语⾔函数绕不开的⼀个话题,那什么是递归呢?     递归其实是⼀种解决问题的⽅法,在C语⾔中,递归就是函数⾃⼰调⽤⾃⼰。 写⼀个史上最简单的C语⾔递归代码:
TANGLONG
2024/10/15
1550
【C语言】函数递归(含扫雷进阶思路)
关于我、重生到500年前凭借C语言改变世界科技vlog.8——函数递归
在 vlog.2 的 printf 函数的返回值举例中,我们使用多次递归的方式实现了同一个函数的返回值调用,但这只是一个简易的递归,不算真正意义上的递归,那么什么是递归?
DARLING Zero two
2024/11/19
1060
关于我、重生到500年前凭借C语言改变世界科技vlog.8——函数递归
【C语言】递归详解
上述就是⼀个简单的递归程序,只不过上⾯的递归只是为了演示递归的基本形式,不是为了解决问题,代码最终也会陷⼊死递归,导致栈溢出。
zxctscl
2024/01/23
8680
【C语言】递归详解
【C语言系列】函数递归
递归其实是一种解决问题的方法,在C语言中,递归就是函数自己调用自己。 下面我们看最简单的递归代码:
四念处茫茫
2025/02/06
1660
【C语言系列】函数递归
c语言从入门到实战——函数递归
函数递归是指一个函数直接或间接地调用自身,以解决问题的一种方法。在C语言中,函数递归可以用来计算阶乘、斐波那契数列等数学问题。 函数递归是一种编程技术,其中函数直接或间接地调用自身来解决问题。它常用于处理可以分解为更小同类问题的复杂问题,如排序、搜索树等。递归的基本思想是将问题分解为更简单的子问题,然后组合子问题的解来得到原问题的解。然而,递归需要小心处理终止条件,否则可能导致无限循环。此外,递归可能消耗大量内存,因为它需要存储每个递归调用的状态。因此,在使用递归时,应仔细考虑其效率和适用性。
鲜于言悠
2024/03/20
2870
c语言从入门到实战——函数递归
C语言学习系列-->【函数的递归】
3、这个盒子里有盒子,而盒子里的盒子又有盒子。钥匙就在某个盒子中。为了找到钥匙,苦逼的你尝试了不同的方法:
南桥
2024/01/26
1300
C语言学习系列-->【函数的递归】
初识C语言·递归
递归,这两字的理解应该分开来理解,递推和回归,在C语言中,递归是函数自己调用自己,最后返回一个结果,比如写一段最简单的递归。
_lazy
2024/10/16
1790
初识C语言·递归
c语言函数递归与迭代详解(含青蛙跳台阶问题详解)
1.递归是什么? 递归是学习C语言函数绕不开的一个话题,那什么是递归呢? 递归其实是一种解决问题的方法,在C语言中,递归就是函数自己调用自己。 这里有一个极其简单的递归代码:
fhvyxyci
2024/09/24
1500
c语言函数递归与迭代详解(含青蛙跳台阶问题详解)
C语言:函数递归
递归的思想: 把⼀个⼤型复杂问题层层转化为⼀个与原问题相似,但规模较小的⼦问题来求解;直到⼦问题不能再被拆分,递归就结束了。所以递归的思考⽅式就是把⼤事化小的过程。
小陈在拼命
2024/02/17
2350
C语言:函数递归
计算机小白的成长历程——函数(4)
大家好,很高兴又和大家见面啦!经过前面几个篇章的学习,我相信大家对函数的基本知识点都已经掌握的差不多了,接下来我们将要开始探讨函数递归的相关内容了。
蒙奇D索隆
2023/10/13
1870
计算机小白的成长历程——函数(4)
C语言--函数递归与迭代
当n≤2时,第n个斐波那契数都是1,当n>2时,第n个斐波那契数就可以通过前两个数相加计算
Undoom
2024/09/23
1270
C语言函数递归_c语言递归举例
大家好,我是架构君,一个会写代码吟诗的架构师。今天说一说C语言函数递归_c语言递归举例,希望能够帮助大家进步!!!
Java架构师必看
2022/07/19
13.9K0
C语言函数递归_c语言递归举例
【C语言基础】:函数递归详解
函数递归指的是在函数内部调用自身的过程。 具体而言,递归函数通过将一个问题分解为更小的、类似的子问题来解决问题。
爱喝兽奶的熊孩子
2024/04/10
1.2K0
【C语言基础】:函数递归详解
函数的递归
1. 递归是什么? 递归是学习C语⾔函数绕不开的⼀个话题,那什么是递归呢? 递归其实是⼀种解决问题的方法,在C语⾔中,递归就是函数⾃⼰调⽤⾃⼰。 写⼀个史上最简单的C语⾔递归代码:
用户11290648
2024/09/25
1560
函数的递归
数据结构与算法 --- 递归(一)
「递归(Recursion)」 是一种解决问题的方法,它将问题分解为更小的子问题,并逐层解决这些子问题。递归算法的核心思想是:「一个函数可以直接或间接地调用自身」。通过这种自我调用,我们可以用简洁的代码来解决复杂问题。
Niuery Diary
2023/10/22
3990
数据结构与算法 --- 递归(一)
函数递归和迭代
函数每次调用都会在栈区申请一定空间 该空间为函数栈帧 函数被调用时申请空间 函数结束后该空间销毁
E绵绵
2024/04/08
1680
函数递归和迭代
【C语言篇】递归详细介绍(基础概念习题及汉诺塔等进阶问题)
递归其实是⼀种解决问题的⽅法,在C语⾔中,递归就是函数⾃⼰调⽤⾃⼰。 写⼀个史上最简单的C语⾔递归代码:
半截诗
2024/10/09
1640
【C语言篇】递归详细介绍(基础概念习题及汉诺塔等进阶问题)
如何深入掌握C语言递归函数(详解)
递归的精髓在于通过不断地重复逼近一个最终的结果,它更多的是一种思想,用于解决某些问题
用户9645905
2022/11/30
8670
如何深入掌握C语言递归函数(详解)
【C语言】卍字通晓→函数+递归
🚀write in front🚀    🔎大家好,我是泽En,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎 🏅2021年度博客之星物联网与嵌入式开发TOP5→周榜43→总榜3343🏅 🆔本文由 泽En 原创 CSDN首发🐒 如需转载还请通知⚠ 📝个人主页:打打酱油desu_泽En_CSDN博客🎓 🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​ 📣系列专栏:【C】系列_打打酱油desu-CSDN博客📢 ✉️我们并非登上我们所选择的舞台,演出并非我们所选择的剧本📩 ---- 目录
謓泽
2022/12/12
7990
【C语言】卍字通晓→函数+递归
推荐阅读
相关推荐
【C语言】函数递归总结
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档