MVCC能否解决幻读
参考:
https://blog.csdn.net/qq_35590091/article/details/107734005
https://blog.csdn.net/ashic/article/details/53735537
https://blog.csdn.net/QAQ123666/article/details/105084758
MVCC能否解决幻读?
“幻读”指,同一个事务里面连续执行两次同样的sql语句,可能导致不同结果的问题,第二次sql语句可能会返回之前不存在的行。
先给出结论:不能笼统的说能不能解决,因为有的情况下可以解决,但是有的情况下解决不了。
可以解决的情况
mysql里面实际上有两种读,一种是“快照读”,比如我们使用select进行查询,就是快照读,在“快照读"的情况下是可以解决“幻读”的问题的。使用的就是MVCC,具体来说如下图,几个事务并发执行:
可以看到,尽管别的事务已经提交插入和更新,但是事务A的select读取的还是一样的。具体就是mvcc利用历史版本信息(快照)来控制他能读取的数据的范围。具体的可以看看我的关于MVCC浅析的文章。
另外一种读是:“当前读”。对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式,此外,下面两个语句也是当前读:
1、select * from table where ? lock in share mode; (加共享锁)
2、select * from table where ? for update; (加排它锁)
因此总结一下,下面几个语句都是当前读,都会读取最新的快照数据,都会加锁(除了第一个加共享锁,其他都是互斥锁):
select * from table where ? lock in share mode; select * from table where ? for update; insert; update; delete;
在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。比如要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。读取的是最新的数据,并且需要加锁(排它锁或者共享锁)。
举个例子,下面是在可重复读级别下,事务1在update后,对该数据加锁,事务B无法插入新的数据,这样事务A在update前后数据保持一致,避免了幻读,可以明确的是,update锁的肯定不只是已查询到的几条数据,因为这样无法阻止insert,有同学会说,那就是锁住了整张表呗,其实不是,其实这里的锁,是next-key locking(就是一个行锁+范围锁)实现的.行锁不必说,就是更新的时候锁住这一行,这样别的事务就不能同时进行修改操作了。范围锁(gap lock)锁则是防止插入。
什么是next key lock?
所谓的next key lock就是一个行锁(record lock)+范围锁(gap lock),比如某一个辅助索引(比如上面的class_id),如果它有1,3,5这几个值,那么当我们使用next key lock的锁住class_id=1的时候,实际上锁住了(-无穷,1],或者锁住class_id=3的时候,实际上锁住的是(1,3],也就是一个左开右闭的区间。如果此时别的事务要在这个区间内插入数据,就会被阻塞住。这个锁一直到事务提交才会释放。因此,即使出现了上面图片里面这种情况,也可以保证前后两次去读的内容一致,因为对这个辅助索引上的锁是:“next key lock”,他会锁住一个区间。
但是注意,对于可重复读默认使用的就是next key lock,但是对于“唯一索引”,比如主键的索引,next key lock会降级成行锁,而不会锁住一个区间。因此,如果上面的事务1的update使用的是主键,事务2也使用主键进行插入,那么实际上事务2根本不会被阻塞,可以立即插入并返回。而对于非唯一索引,next key lock则不会降级。
什么情况MVCC也会出现幻读?
下面这样的情况:
1.a事务先select,b事务insert确实会加一个gap锁,但是如果b事务commit,这个gap锁就会释放(释放后a事务可以随意操作),
2.a事务再select出来的结果在MVCC下还和第一次select一样,
3.接着a事务不加条件地update,这个update会作用在所有行上(包括b事务新加的),
4.a事务再次select就会出现b事务中的新行,并且这个新行已经被update修改了.
上面这样,事务2提交之后,事务1再次执行update,因为这个是当前读,他会读取最新的数据,包括别的事务已经提交的,所以就会导致此时前后读取的数据不一致,出现幻读。
参考:
https://www.cnblogs.com/CoderAyu/p/11525408.html
Mysql(Innodb)如何避免幻读
幻读Phantom Rows
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
幻读问题是指一个事务的两次不同时间的相同查询返回了不同的的结果集。例如:一个 select 语句执行了两次,但是在第二次返回了第一次没有返回的行,那么这些行就是“phantom” row.
read view(或者说 MVCC)实现了一致性不锁定读(Consistent Nonlocking Reads),从而避免了(非当前读下)幻读
实验1:
开两个窗口设置
set session tx_isolation='REPEATABLE-READ';
select @@session.autocommit;select @@global.tx_isolation,@@session.tx_isolation;
create table read_view(text varchar(50));
insert into read_view values('init');
两个会话开始事务
SESSION_A>begin;
Query OK, 0 rows affected (0.00 sec)
SESSION_B>begin;
Query OK, 0 rows affected (0.00 sec)
SESSION_A执行一个查询,这个查询可以访问任何表,这个查询的目的是创建一个当前时间点的快照START TRANSACTION WITH CONSISTENT SNAPSHOT;
也可以达到同样的效果
SESSION_A>select * from dept;
+--------+------------+----------+
| deptno | dname | loc |
+--------+------------+----------+
| 10 | ACCOUNTING | NEW YORK |
| 20 | RESEARCH | DALLAS |
| 30 | SALES | CHICAGO |
| 40 | OPERATIONS | BOSTON |
+--------+------------+----------+
4 rows in set (0.00 sec)
SESSION_B 插入一条记录并提交
SESSION_B>insert into read_view values('after session A select');
Query OK, 1 row affected (0.01 sec)
SESSION_B>commit;
Query OK, 0 rows affected (0.00 sec)
SESSION_A
SESSION_A>select * from read_view;
+------+
| text |
+------+
| init |
+------+
1 row in set (0.00 sec)
SESSION_A>commit;
Query OK, 0 rows affected (0.00 sec)
SESSION_A>select * from read_view;
+------------------------+
| text |
+------------------------+
| init |
| after session A select |
+------------------------+
2 rows in set (0.00 sec)
由于 SESSION_A 第一次的查询开始于 SESSION_B 插入数据前,所以创建了一个以SELECT操作的时间为基准点的 read view,避免了幻读的产生
所以在 SESSION_A 的事务结束前,无法看到 SESSION_B 对表 read_view 做出的任何更改 (insert,delete,update)
实验2
两个会话开始事务
SESSION_A>begin;
Query OK, 0 rows affected (0.00 sec)
SESSION_B>begin;
Query OK, 0 rows affected (0.00 sec)
SESSION_B 在 SESSION_A 创建read view 前插入数据
SESSION_B>insert into read_view values('before Session_A select');
Query OK, 1 row affected (0.00 sec)
SESSION_B>commit;
Query OK, 0 rows affected (0.00 sec)
SESSION_A
SESSION_A>select * from read_view;
+-------------------------+
| text |
+-------------------------+
| init |
| after session A select |
| before Session_A select |
+-------------------------+
3 rows in set (0.00 sec)
SESSION_A>commit
-> ;
Query OK, 0 rows affected (0.00 sec)
SESSION_A>select * from read_view;
+-------------------------+
| text |
+-------------------------+
| init |
| after session A select |
| before Session_A select |
+-------------------------+
3 rows in set (0.00 sec)
由于 SESSION_A 第一次查询开始于 SESSION_B 对表做出更改并提交后,所以这次的 read view 包含了 SESSION_B 所做出的更改
在官方文档中写道
http://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html
A consistent read means that InnoDB uses multi-versioning to present to a query a snapshot of the database at a point in time. The query sees the changes made by transactions that committed before that point of time, and no changes made by later or uncommitted transactions. The exception to this rule is that the query sees the changes made by earlier statements within the same transaction. This exception causes the following anomaly: If you update some rows in a table, a SELECT sees the latest version of the updated rows, but it might also see older versions of any rows. If other sessions simultaneously update the same table, the anomaly means that you might see the table in a state that never existed in the database.
一致性读是通过 MVCC 为查询提供了一个基于时间的点的快照。这个查询只能看到在自己之前提交的数据,而在查询开始之后提交的数据是不可以看到的。一个特例是,这个查询可以看到于自己开始之后的同一个事务产生的变化。这个特例会产生一些反常的现象
If the transaction isolation level is REPEATABLE READ (the default level), all consistent reads within the same transaction read the snapshot established by the first such read in that transaction. You can get a fresher snapshot for your queries by committing the current transaction and after that issuing new queries.
在默认隔离级别REPEATABLE READ下,同一事务的所有一致性读只会读取第一次查询时创建的快照
实验3
两个会话开始事务
SESSION_A开始事务并创建快照
SESSION_A>START TRANSACTION WITH CONSISTENT SNAPSHOT;
Query OK, 0 rows affected (0.00 sec)
SESSION_B>begin;
Query OK, 0 rows affected (0.00 sec)
SESSION_A>select * from read_view;
+-------------------------+
| text |
+-------------------------+
| init |
| after session A select |
| before Session_A select |
+-------------------------+
3 rows in set (0.00 sec)
SESSION_B>insert into read_view values('anomaly'),('anomaly');
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
SESSION_B>update read_view set text='INIT' where text='init';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
SESSION_B>commit;
Query OK, 0 rows affected (0.00 sec)
SESSION_A>select * from read_view;
+-------------------------+
| text |
+-------------------------+
| init |
| after session A select |
| before Session_A select |
+-------------------------+
3 rows in set (0.00 sec)
SESSION_A更新了它并没有"看"到的行
SESSION_A>update read_view set text='anomaly!' where text='anomaly';
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2 Changed: 2 Warnings: 0
SESSION_A>select * from read_view;
+-------------------------+
| text |
+-------------------------+
| init |
| after session A select |
| before Session_A select |
| anomaly! |
| anomaly! |
+-------------------------+
5 rows in set (0.00 sec)
SESSION_A>commit;
Query OK, 0 rows affected (0.00 sec)
SESSION_A>select * from read_view;
+-------------------------+
| text |
+-------------------------+
| INIT |
| after session A select |
| before Session_A select |
| anomaly! |
| anomaly! |
+-------------------------+
5 rows in set (0.00 sec)
观察实验步骤可以发现,在倒数第二次查询中,出现了一个并不存在的状态
the anomaly means that you might see the table in a state that never existed in the database
这里A的前后两次读,均为快照读,而且是在同一个事务中。但是B先插入直接提交,此时A再update,update属于当前读,所以可以作用于新插入的行,并且将修改行的当前版本号设为A的事务号,所以第二次的快照读,是可以读取到的,因为同事务号。这种情况符合MVCC的规则,如果要称为一种幻读也非不可,算为一个特殊情况来看待吧。
With READ COMMITTED isolation level, each consistent read within a transaction sets and reads its own fresh snapshot.
在 read commit 隔离级别下,同一事务的每个一致性读sets and reads its own fresh snapshot.
实验4
修改事务隔离级别set session tx_isolation='READ-COMMITTED'
两个会话开始事务
SESSION_A>begin;
Query OK, 0 rows affected (0.00 sec)
SESSION_B>begin;
Query OK, 0 rows affected (0.00 sec)
SESSION_A>select * from read_view;
+-------------------------+
| text |
+-------------------------+
| INIT |
| after session A select |
| before Session_A select |
| anomaly! |
| anomaly! |
+-------------------------+
5 rows in set (0.00 sec)
SESSION_B>insert into read_view values('hehe');
Query OK, 1 row affected (0.00 sec)
SESSION_B>commit;
Query OK, 0 rows affected (0.00 sec)
SESSION_A>select * from read_view;
+-------------------------+
| text |
+-------------------------+
| INIT |
| after session A select |
| before Session_A select |
| anomaly! |
| anomaly! |
| hehe |
+-------------------------+
6 rows in set (0.00 sec)
read commit 每次读取都是新的快照
InnoDB通过Nextkey lock解决了当前读时的幻读问题
Innodb行锁分为:
类型 | 说明 |
---|---|
Record Lock: | 在索引上对单行记录加锁. |
Gap Lock: | 锁定一个范围的记录,但不包括记录本身.锁加在未使用的空闲空间上,可能是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间. |
Next-Key Lock: | 行锁与间隙锁组合起来用就叫做Next-Key Lock。锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。 |
实验5
创建表
(mysql@localhost) [fandb]> create table t5(id int,key(id));
Query OK, 0 rows affected (0.02 sec)
SESSION_A>insert into t5 values(1),(4),(7),(10);
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0
开始实验
SESSION_A>begin;
Query OK, 0 rows affected (0.00 sec)
SESSION_A>select * from t5;
+------+
| id |
+------+
| 1 |
| 4 |
| 7 |
| 10 |
+------+
4 rows in set (0.00 sec)
SESSION_A>select * from t5 where id=7 for update;
+------+
| id |
+------+
| 7 |
+------+
1 row in set (0.00 sec)
SESSION_B>begin;
Query OK, 0 rows affected (0.00 sec)
SESSION_B>insert into t5 values(2);
Query OK, 1 row affected (0.00 sec)
SESSION_B>insert into t5 values(12);
Query OK, 1 row affected (0.00 sec)
SESSION_B>insert into t5 values(5); --被阻塞
^CCtrl-C -- sending "KILL QUERY 93" to server ...
Ctrl-C -- query aborted.
^[[AERROR 1317 (70100): Query execution was interrupted
SESSION_B>insert into t5 values(7); --被阻塞
^CCtrl-C -- sending "KILL QUERY 93" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted
SESSION_B>insert into t5 values(9); --被阻塞
^CCtrl-C -- sending "KILL QUERY 93" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted
SESSION_B>commit;
Query OK, 0 rows affected (0.00 sec)
SESSION_A>select * from t5;
+------+
| id |
+------+
| 1 |
| 4 |
| 7 |
| 10 |
+------+
4 rows in set (0.00 sec)
SESSION_A>commit;
Query OK, 0 rows affected (0.00 sec)
SESSION_A>select * from t5;
+------+
| id |
+------+
| 1 |
| 2 |
| 4 |
| 7 |
| 10 |
| 12 |
+------+
6 rows in set (0.00 sec)
当以当前读模式select * from t5 where id=7 for update;
获取 id=7的数据时,产生了 Next-Key Lock,锁住了4-10范围和 id=7单个record
从而阻塞了 SESSION_B在这个范围内插入数据,而在除此之外的范围内是可以插入数据的。
在倒数第二个查询中,因为 read view 的存在,避免了我们看到 2和12两条数据,避免了幻读
同时因为 Next-Key Lock 的存在,阻塞了其他回话插入数据,因此当前模式读不会产生幻读(select for update 是以当前读模式获取数据)
###尽量使用唯一索引,因为唯一索引会把Next-Key Lock降级为Record Lock
实验6
创建表
(mysql@localhost) [fandb]> create table t6(id int primary key);
Query OK, 0 rows affected (0.02 sec)
SESSION_A>insert into t6 values(1),(4),(7),(10);
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0
开始实验
SESSION_A>begin;
Query OK, 0 rows affected (0.00 sec)
SESSION_A>select * from t6;
+----+
| id |
+----+
| 1 |
| 4 |
| 7 |
| 10 |
+----+
4 rows in set (0.00 sec)
SESSION_A>select * from t6 where id=7 for update;
+----+
| id |
+----+
| 7 |
+----+
1 row in set (0.00 sec)
SESSION_B>begin;
Query OK, 0 rows affected (0.00 sec)
SESSION_B>insert into t6 values(5); --插入成功没有阻塞
Query OK, 1 row affected (0.00 sec)
SESSION_B>insert into t6 values(8); --插入成功没有阻塞
Query OK, 1 row affected (0.00 sec)
SESSION_B>commit;
Query OK, 0 rows affected (0.00 sec)
SESSION_A>select * from t6;
+----+
| id |
+----+
| 1 |
| 4 |
| 7 |
| 10 |
+----+
4 rows in set (0.00 sec)
SESSION_A>commit;
Query OK, 0 rows affected (0.00 sec)
SESSION_A>select * from t6;
+----+
| id |
+----+
| 1 |
| 4 |
| 5 |
| 7 |
| 8 |
| 10 |
+----+
6 rows in set (0.00 sec)
当 id 列有唯一索引,Next-Key Lock 会降级为 Records Lock
MySQL解决幻读——MVCC与间隙锁
当前读 与 快照读
当前读:
select...lock in share mode (共享读锁)
select...for update
update , delete , insert
当前读, 读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题。
例如,假设要update一条记录,但是另一个事务已经delete这条数据并且commit了,如果不加锁就会产生冲突。所以update的时候肯定要是当前读,得到最新的信息并且锁定相应的记录。
快照读:
简单的select操作(不包括 select … lock in share mode, select … for update)。
Read Committed隔离级别:每次select都生成一个快照读。
Read Repeatable隔离级别:开启事务后第一个select语句才是快照读的地方,而不是一开启事务就快照读。
在RR级别下,快照读是通过MVVC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。
如何解决幻读
快照读的幻读是用MVCC解决的,当前的读的幻读是用间隙锁解决的。
innodb的默认事务隔离级别是rr(可重复读)。它的实现技术是mvcc。该技术不仅可以保证innodb的可重复读,而且可以防止幻读。(这也就是是此前以rr隔离级别实践时,不仅可以防止可重复读,也防止了幻读)但是它防止的是快照读,也就是读取的数据虽然是一致的,但是数据是历史数据。
这个帖子里面就有一个实例:MySQL的InnoDB的幻读问题
一些文章写到InnoDB的可重复读避免了“幻读”(phantom read),这个说法并不准确。
那InnoDB指出的可以避免幻读是怎么回事呢?
以下翻译自MySQL官网文档(http://download.nust.na/pub6/mysql/doc/refman/5.5/en/innodb-next-key-locking.html),翻译水平一般,请见谅。
当隔离级别是可重复读,且禁用innodb_locks_unsafe_for_binlog的情况下,在搜索和扫描index的时候使用的next-key locks可以避免幻读。也就是说的间隙锁。
InnoDB提供了next-key locks,但需要应用程序自己去加锁
举个例子:
SELECT * FROM child WHERE id> 100 FOR UPDATE;
该查询从id大于100 的第一条记录开始扫描索引 。如果该表包含具有id在90和102值的行。如果在扫描范围内的索引记录上设置的锁没有锁定在间隙中插入的内容(在这种情况下,在介于90和102之间,则另一个事务可以在表中插入一个新行,其行号id为101。如果您要在同一个事务中执行相同的SELECT,您将在查询返回的结果集中看到一个id为101的新行(一个“幻影”),这就产生了幻读。这与事务的隔离原则是相反的:一个事务应该能够运行,以便它已经读的数据在事务过程中不改变。如果我们把一套行视为数据项,新的“幽灵”子记录可能会违反这一隔离原则。
为了防止“幻影”,InnoDB使用了一种名为next-key locking
的算法,它将 索引行锁定 和 间隙锁定 结合在一起。InnoDB执行行级锁的方式是,当它搜索或扫描一个表索引时,它会在遇到的索引记录上设置共享或独占锁。因此,行级锁实际上是索引记录锁。此外,索引记录上的next-key lock 也会影响该索引记录之前的“间隙”。也就是说,next-key lock 是一个index-record lock(索引记录锁)加上一个在索引记录之前的间隙上的 gap lock (间隙锁)。如果一个会话在索引中的记录R上具有共享锁或独占锁,则另一个会话不能在R之前的间隙中插入新的索引记录。
当InnoDB扫描一个索引之时,它也锁定所有记录中最后一个记录之后的间隙。刚在前一个例子中发生:InnoDB设置的锁定防止任何插入到id可能大过100的表。所有插入id为101的数据是无法执行的
您可以使用 next-key lock在应用程序中实现唯一性检查:如果你读数据时加了共享锁(select … from lock in share mode; 生成表级共享锁,允许其它线程读取数据但不能修改数据。)和没有看到你要复制的行插入,那么您可以安全地插入行并且明白:在读取期间,对您的行的后续行设置的next-key锁将防止任何人同时为您的行插入副本。因此,next-key锁允许您“锁定”表中不存在的内容。
假设我们有一个表:city,结构如下:
在第一个查询窗口中开始一个事务:
事务A | 事务B |
---|---|
BEGIN; | |
SELECT * FROM city WHERE id > 2 |
|
BEGIN; | |
INSERT INTO city VALUES (6, ‘成都’); | |
COMMIT; | |
SELECT * FROM city WHERE id > 2 |
|
SELECT * FROM city WHERE id > 2 LOCK IN SHARE MODE |
问,事务A的三个select分别是什么结果:
第一个:南京,广州,杭州
第二个:南京,广州,杭州
第三个:南京,广州,杭州,成都
我们可以看出,如果使用普通的读,会得到一致性的结果,如果使用了加锁的读,就会读到“最新的”“提交”读的结果。如果需要实时显示数据,还是需要通过手动加锁来实现。这个时候会使用next-key技术来实现。
本身,可重复读和提交读是矛盾的。在同一个事务里,如果保证了可重复读,就会看不到其他事务的提交,违背了提交读;如果保证了提交读,就会导致前后两次读到的结果不一致,违背了可重复读。InnoDB提供了这样的机制,在默认的可重复读的隔离级别里,可以使用加锁读去查询最新的数据: IN SHARE MODE;
我们再开启一个事务,执行:
BEGIN;
INSERT INTO city VALUES (7, '济南');
COMMIT;
会怎样?
结果是阻塞。因为加了间隙锁
我们再开启一个事务,执行:
BEGIN;
UPDATE city SET name = '济南' WHERE ID = 1
UPDATE city SET name = '济南' WHERE ID = 2
COMMIT;
会怎样?
修改成功,没问题,间隙锁没加到这。
如果我写
BEGIN;
UPDATE city SET name = '济南' WHERE ID = 1
肯定会阻塞,因为间隙锁的原因。
结论:MySQL InnoDB的可重复读并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁度使用到的机制就是next-key locks。
在mysql中,提供了两种事务隔离技术,第一个是mvcc,第二个是next-key技术。这个在使用不同的语句的时候可以动态选择。不加lock inshare mode之类的快照读就使用mvcc。否则 当前读使用next-key。mvcc的优势是不加锁,并发性高。缺点是不是实时数据。next-key的优势是获取实时数据,但是需要加锁。
另外,重要:
在rr级别下,mvcc完全解决了重复读,但并不能真正的完全避免幻读,只是在部分场景下利用历史数据规避了幻读
对于快照读,mysql使用mvcc利用历史数据部分避免了幻读(在某些场景看上去规避了幻读)
要完全避免,需要手动加锁将快照读调整为当前读(mysql不会自动加锁),然后mysql使用next-key完全避免了幻读,比如rr下,锁1(0,2,3,4),另一个线程的insert 3即被阻塞,在rc下,另一个线程仍然可以大摇大摆的插入,如本线程再次查询比如count,则会不一致
建议去看官方文档。
参考文章:
innodb当前读 与 快照读
MySQL的InnoDB的幻读问题
相关的官方文档
MySQL的锁机制 - 记录锁、间隙锁、临键锁
【MySQL】当前读、快照读、MVCC