mysql的mvcc 和next-lock

て烟熏妆下的殇ゞ 提交于 2019-12-16 04:24:09

mysql在rr隔离级别下,是如何解决幻读的?
mysql通过mvcc 及加上next-key lock来解决幻读发生的。
很多资料写的是rr隔离级别下是无法避免幻读的,的确是,理论上的rr是无法避免幻读的,但是mysql的innodb引擎通过next-key解决了幻读问题 (select 可以使用for update /lock in share mode来解决幻读)。

mvcc原理
数据行有另外两列,一个是create-version,delete-version,数据创建的时候初始化create-version,被delete及update时,设置其delete-version,值都是当前事务ID
比如我插入一条记录, 事务id 假设是1 ,那么记录如下:也就是说,创建版本号就是事务版本号。在这里插入图片描述
如果我更新的话,事务id假设是2
在这里插入图片描述
如果我删除的话,假设事务是id=3
在这里插入图片描述
要注意,必须是commit的时候才会修改create-version delete-version字段,在没有提交的情况是,是不会修改和创建的。

这样保证了在 a b 两个事务情况下,假设
a(transationid=1)
b(transactionid=2)
由于事务a早于事务b
约束:
1.事务只能读取到delete-version为空,或者delete版本号小于当前事务的数据行
2.事务只能读取到create-version小于当前事务的数据行。

结论:由于事务只能读取到create-version小于自己事务ID的数据,所以保证了事务不会读取早之后事务insert的数据;2。由于事务只能读取到delete-version大于自己或为空的数据,所以对于之后事务update或者delete的数据是无感知的,这样保证了可重复读。

但是mvcc如何保证对于范围查询,数据一致呢?

实验1:
事务a 执行查询:
begin;
select * from test where id.100 and id<150;

事务b之后执行了insertinto ,并commit

那么事务a再次执行 select * from test where id.100 and id<150; ,不可能会拿到事务b的数据。因为事务b的create-version明显大于了事务a,因此无法读取到insert的数据。

实验2:
事务a先开始:
begin
select * from test where id>80
事务b:
begin;
insert into test(id,name,age) values (83,‘xx’,11);
commit;
事务a:
select * from test where id>80 //没有出现83
insert into test(id,name,age) values (83,‘xx’,11);//报错,可是上面明明提示没有83啊
出现了幻读问题
实验3:
事务a执行:
begin;
select * from test; //只有一行
事务b:
begin
insert into test(id,name,age) values (2,‘xx’,1);
commit;
事务a:
select * from test; //还是只有一行
事务c:
begin
insert into test(id,name,age) values (3,‘xx’,1);
commit;
事务a:
update test set age= 22 ; //Rows matched: 3 Changed: 5 Warnings: 0
select * from test; //变成了3行???
幻读发生了。。
事务d:
insert into test(id,name,age) values (5,‘xx’,1); //被卡住了(因为事务a之前一直没有用锁,现在升级为表锁了,可能会升级为间隙锁,注意如果间隙锁没锁住,其他事务还是可以继续insert的,如果被锁的区间和其他事务已经commit的数据冲撞,事务a会更新数据,即select的数据会变,幻读发生)

幻读问题如何解决?
mysql使用了间隙锁来避免这个问题,当使用了lock in share mode或者for update时,或者update,delete时,innodb默认会使用间隙锁,进行区间加锁,这个时候事务b执行insert的时候会被卡住,直到事务a commit成功。
因此使用间隙锁可以避免幻读的发生的,但是在不用锁的情况下,肯定会有幻读的发生。

实验3:
事务a:
begin;
select * from test; //empty
事务b:
begin;
insert into test(name,age) values (‘xx’,11);
commit;
事务a:
select * from test ;// empty
select * from test where id>0 lock in share mode; //升级使用next-key lock,获取到最新数据,但是导致了不可重读问题。
commit;
虽然使用for update /lock in share mode可以读取到最新的数据,但是却导致了不可重复读的问题。

总结:
1.单纯使用mvcc版本控制虽然可以避免一部分的快照读带来的幻读问题,但是对于当前读还是无法避免幻读的发生的,所以使用next-key锁应运而生了。
2. rr级别下,对于select情况可以保证可重复读,由于mvcc原因可以保证,但是无法读取到最新的数据,如果使用lock in share mode或者for update即可以得到最新的数据。
3. 如果使用普通的读,会得到一致性的结果,如果使用了加锁的读,就会读到“最新的”“提交”读的结果。

本身,可重复读和提交读是矛盾的。在同一个事务里,如果保证了可重复读,就会看不到其他事务的提交,违背了提交读;如果保证了提交读,就会导致前后两次读到的结果不一致,违背了可重复读。

可以这么讲,InnoDB提供了这样的机制,在默认的可重复读的隔离级别里,可以使用加锁读去查询最新的数据。

http://dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html

If you want to see the “freshest” state of the database, you should use either the READ COMMITTED isolation level or a locking read:
SELECT * FROM t_bitfly LOCK IN SHARE MODE;

from:
https://blog.csdn.net/qq_33330687/article/details/89004462
https://blog.csdn.net/weixin_30618985/article/details/95072958
幻读实验:http://blog.sina.com.cn/s/blog_499740cb0100ugs7.html
lock in share mode读最新实验:https://www.cnblogs.com/crazylqy/p/7614092.html
问题引出
我之前的一篇博客 数据库并发不一致分析 有提到过事务隔离级别以及相应加锁方式、能够解决的并发问题。

标准情况下,在 RR(Repeatable Read) 隔离级别下能解决不可重复读(当行修改)的问题,但是不能解决幻读的问题。

而之前有看过一篇 mysql 加锁的文章 MySQL 加锁处理分析,里面有提到一点:

对于Innodb,Repeatable Read (RR) 针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象。

那么问题来了,到底 Innodb 中 RR 隔离级别是否能解决幻读呢?

在 MySQL 加锁处理分析这篇文章下面的评论中,有这样的一个交流:

ontheway
弱弱地问一句,我看的书里面都说的是RR隔离级别不允许脏读和不可重复读,但是可以幻读,怎么和作者说的不一样呢?

hedengcheng(作者)
你说的没错,因此我在文章一开始,就强调了这一点。mysql innodb引擎的实现,跟标准有所不同。

求证官方文档
MySQL Innodb 引擎的实现,跟标准有所不同,针对这个问题,我表示怀疑,于是查看 mysql 官方文档关于 RR的解释,里面有这么一段话:

For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE), UPDATE, and DELETE statements, locking depends on whether the statement uses a unique index with a unique search condition, or a range-type search condition. For a unique index with a unique search condition, InnoDB locks only the index record found, not the gap before it. For other search conditions, InnoDB locks the index range scanned, using gap locks or next-key locks to block insertions by other sessions into the gaps covered by the range.

大致意思就是,在 RR 级别下,如果查询条件能使用上唯一索引,或者是一个唯一的查询条件,那么仅加行锁,如果是一个范围查询,那么就会给这个范围加上 gap 锁或者 next-key锁 (行锁+gap锁)。

从这句话的理解来看,和文章里的解释一样,由于 RR 级别对于范围会加 GAP 锁,这个和 sql 的标准是有一些差异的。

其他解释
后面又发现了一篇文章 Understanding InnoDB transaction isolation levels,文章中又提到:

This isolation level is the default for InnoDB. Although this isolation level solves the problem of non-repeatable read, but there is another possible problem phantom reads.

大概意思是,RR 能解决不可重复读的问题,但仍可能发生幻读,怀疑作者并不了解 Innodb 的特殊实现,评论中也有提到:

Do you mean ‘write skew’ instead of ‘phantom reads’? The ‘repeatable read’ in SQL standard allows ‘phantom reads’, however, since InnoDB uses next-key locking this anomaly does not exist in this level. Looks like it’s equivalent to ‘snapshot isolation’ in Postgres and Oracle.

再来看一篇文章 MySQL的InnoDB的幻读问题,这里面提供了一些例子,还没来得及分析,但最后的结论是:

MySQL InnoDB的可重复读并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁度使用到的机制就是next-key locks。

最终结论
Innodb 的 RR 隔离界别对范围会加上 GAP,理论上不会存在幻读,但是是否有例外呢,这个还需要进一步求证。

转载于:https://www.cnblogs.com/likui360/p/9632641.html

初识MySQL的gap,觉得这个设计比较独特,和其他数据库的做法不太一样,所以整理一个简单的memo(虽然关于gap锁,相关资料已经很多了)

  1. 什么是gap
    A place in an InnoDB index data structure where new values could be inserted.
    说白了gap就是索引树中插入新记录的空隙。相应的gap lock就是加在gap上的锁,还有一个next-key锁,是记录+记录前面的gap的组合的锁。

  2. gap锁或next-key锁的作用
    http://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html

To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row
locking with gap locking. InnoDB performs row-level locking in such a way that when it searches
or scans a table index, it sets shared or exclusive locks on the index records it encounters.
Thus, the row-level locks are actually index-record locks. In addition, a next-key lock on
an index record also affects the “gap” before that index record. That is, a next-key lock is
an index-record lock plus a gap lock on the gap preceding the index record. If one session has
a shared or exclusive lock on record R in an index, another session cannot insert a new index
record in the gap immediately before R in the index order.
简单讲就是防止幻读。通过锁阻止特定条件的新记录的插入,因为插入时也要获取gap锁(Insert Intention Locks)。

  1. 什么时候会取得gap lock或nextkey lock
    这和隔离级别有关,只在REPEATABLE READ或以上的隔离级别下的特定操作才会取得gap lock或nextkey lock。

http://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html

2.1 REPEATABLE READ

… For consistent reads, there is an important difference from the READ COMMITTED isolation level:
All consistent reads within the same transaction read the snapshot established by the first read. …

For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE), UPDATE, and DELETE statements,
locking depends on whether the statement uses a unique index with a unique search condition,
or a range-type search condition. For a unique index with a unique search condition,
InnoDB locks only the index record found, not the gap before it. For other search conditions,
InnoDB locks the index range scanned, using gap locks or next-key locks to block insertions
by other sessions into the gaps covered by the range.
locking reads,UPDATE和DELETE时,除了对唯一索引的唯一搜索外都会获取gap锁或next-key锁。即锁住其扫描的范围。

下面对非唯一索引做个测试。

表定义如下:

复制代码
mysql> show create table tb2;
±------±-----------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
±------±-----------------------------------------------------------------------------------------------------------------------------------------------+
| tb2 | CREATE TABLE tb2 (
id int(11) DEFAULT NULL,
c1 int(11) DEFAULT NULL,
KEY tb2_idx1 (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 |
±------±-----------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
复制代码
注意id只是索引,不是主键

表中有3条记录: 10,20,30。

复制代码
mysql> select * from tb2;
±-----±-----+
| id | c1 |
±-----±-----+
| 10 | 0 |
| 20 | 0 |
| 30 | 0 |
±-----±-----+
3 rows in set (0.01 sec)
复制代码

在REPEATABLE READ下,更新一条记录不提交,然后看看能阻塞另外的会话哪些操作。

SESSION 1:

SESSION 1中更新id=20的记录

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update tb2 set c1=2 where id=20;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1 Changed: 1 Warnings: 0

SESSION 2:

SESSION 2中,执行插入操作,发现[10,30)范围不能插入数据。

复制代码
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into tb2 values(9,4);
Query OK, 1 row affected (0.00 sec)

mysql> insert into tb2 values(10,4);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into tb2 values(19,4);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into tb2 values(20,4);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into tb2 values(21,4);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into tb2 values(29,4);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into tb2 values(30,4);
Query OK, 1 row affected (0.01 sec)
复制代码

复制代码
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update tb2 set c1=4 where id=10;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1 Changed: 0 Warnings: 0

mysql> update tb2 set c1=4 where id=20;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> update tb2 set c1=4 where id=30;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 2 Changed: 0 Warnings: 0
复制代码

如果SESSION 1的表扫描没有用到索引,那么gap或next-key锁住的范围是整个表,即任何值都不能插入。

2.2 READ COMMITTED

For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE), UPDATE statements,
and DELETE statements, InnoDB locks only index records, not the gaps before them,
and thus permits the free insertion of new records next to locked records.
只会锁住已有记录,不会加gap锁。

2.3 SERIALIZABLE

This level is like REPEATABLE READ, but InnoDB implicitly converts all plain
SELECT statements to SELECT … LOCK IN SHARE MODE if autocommit is disabled.
和REPEATABLE READ的主要区别在于把普通的SELECT变成SELECT … LOCK IN SHARE MODE,即对普通的select都会获取gap锁或next-key锁。

  1. REPEATABLE READ和幻读
    在“consistent-read”时,REPEATABLE READ下看到是事务开始时的快照,即使其它事务插入了新行通常也是看不到的,所以在常见的场合可以避免幻读。 但是,"locking read"或更新,删除时是会看到已提交的修改的,包括新插入的行。

http://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html

If you want to see the “freshest” state of the database, use either the READ COMMITTED
isolation level or a locking read:
下面看一个例子

SESSION 1:

复制代码
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> select id,c1 from tb1 where id=1;
±—±-----+
| id | c1 |
±—±-----+
| 1 | 100 |
±—±-----+
1 row in set (0.00 sec)
复制代码

SESSION 2:

mysql> update tb1 set c1=101 where id =1;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0

SESSION 1:

复制代码
mysql> select id,c1 from tb1 where id=1 LOCK IN SHARE MODE;
±—±-----+
| id | c1 |
±—±-----+
| 1 | 101 |
±—±-----+
1 row in set (0.00 sec)

mysql> select id,c1 from tb1 where id=1;
±—±-----+
| id | c1 |
±—±-----+
| 1 | 100 |
±—±-----+
1 row in set (0.00 sec)

mysql> update tb1 set c1=c1+1000 where id=1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> select id,c1 from tb1 where id=1;
±—±-----+
| id | c1 |
±—±-----+
| 1 | 1101 |
±—±-----+
1 row in set (0.00 sec)
复制代码

上面update的行为违反了REPEATABLE READ的承诺,看到了事务开始后其它事务的并发更新。这对应用开发需要特别注意,这种情况下其它数据库通常都是报错的。

  1. 其它
    RR和RC相比还有一个重要的区别,RC下,扫描过但不匹配的记录不会加锁,或者是先加锁再释放,即semi-consistent read。但RR下扫描过记录都要加锁。这个差别对有全表扫描的更新的场景影响极大。详细参考http://hedengcheng.com/?p=771,关于MySQL的加锁处理,这篇文章讲得很透彻!
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!