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

[REPEATABLE READ]

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

set global transaction isolation level REPEATABLE READ ;
set session transaction isolation level REPEATABLE READ ; 
复制代码

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

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

START TRANSACTION;
① SELECT sleep(5);
② UPDATE users SET state=1 WHERE id=1;
COMMIT;
复制代码

事务2算法

START TRANSACTION;
① SELECT * FROM users WHERE id=1;
② SELECT sleep(10);
③ SELECT * FROM users WHERE id=1;
COMMIT;  
复制代码

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

[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的执行信息数据库

[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
复制代码

执行结果安全

mark
mark
结论: 可重复读[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。数据是可重复读的。session

从事务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

START TRANSACTION;
SELECT * FROM users;
SELECT sleep(10);
SELECT * FROM users;
COMMIT;
复制代码

事务2

START TRANSACTION;
SELECT sleep(5);
INSERT INTO users VALUES(2,'song',2);
COMMIT;
复制代码

执行结果

1.预期结果

mark
mark

2.实际结果

mark
mark

事务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字段加上索引。而后准备几条数据,以下图:

mark
事务1

START TRANSACTION;  
① SELECT * FROM users WHERE state=3 for UPDATE;
复制代码

事务2

[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释放才可写入,这种方式读取的数据是实时的。

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

事务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;
复制代码

执行结果

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


做者: 撸码那些事
声明:本文为博主学习感悟总结,水平有限,若是不当,欢迎指正。若是您认为还不错,不妨关注一下个人 微信公众号,第一时间获取文章更新。转载与引用请注明出处。
相关文章
相关标签/搜索