这里给出 mysql 幻读的比较形象的场景:mysql
users: id 主键sql
1、T1:select * from users where id = 1; 2、T2:insert into `users`(`id`, `name`) values (1, 'big cat'); 3、T1:insert into `users`(`id`, `name`) values (1, 'big cat');
T1 :主事务,检测表中是否有 id 为 1 的记录,没有则插入,这是咱们指望的正常业务逻辑。spa
T2 :干扰事务,目的在于扰乱 T1 的正常的事务执行。3d
在 RR 隔离级别下,一、2 是会正常执行的,3 则会报错主键冲突,对于 T1 的业务来讲是执行失败的,这里 T1 就是发生了幻读,由于T1读取的数据状态并不能支持他的下一步的业务,见鬼了同样。code
在 Serializable 隔离级别下,1 执行时是会隐式的添加 gap 共享锁的,从而 2 会被阻塞,3 会正常执行,对于 T1 来讲业务是正确的,成功的扼杀了扰乱业务的T2,对于T1来讲他读取的状态是能够拿来支持业务的。blog
因此 mysql 的幻读并不是什么读取两次返回结果集不一样,而是事务在插入事先检测不存在的记录时,惊奇的发现这些数据已经存在了,以前的检测读获取到的数据如同鬼影通常。事务
这里要灵活的理解读取的意思,第一次select是读取,第二次的 insert 其实也属于隐式的读取,只不过是在 mysql 的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入。it
不可重复读侧重表达 读-读,幻读则是说 读-写,用写来证明读的是鬼影。io
下面给出幻读的例子:table
设置隔离级别为 Read Repeatable,开启两个事务 t1和t2
原始表以下:
在t1中,首先查看id=C的数据,为空:
mysql> select * from amount where id = 'C'; Empty set (0.00 sec)
而后在t2中插入id='C'的数据,此时咱们发现插入成功了
mysql> insert into amount values('C',1000); Query OK, 1 row affected (0.01 sec)
而后,咱们在t1中插入id='C'的数据,能够看到一直处于等待状态,直到t2的事务被提交或者超时。
当t2的事务被提交后,t1中会报主键重复的错误:
mysql> insert into amount values('C',1000); ERROR 1062 (23000): Duplicate entry 'C' for key 'PRIMARY'
这就是幻读现象,咱们在t1中查询id='C'的数据时显示数据不存在,可是因为t2中插入了id=C的数据,致使了t1再想插入时出现了主键重复的错误,t2成功扰乱了t1的事务。
咱们看看再Serializable的级别下是如何的:
在t1中,首先查看id=C的数据,为空:
mysql> select * from amount where id = 'C'; Empty set (0.00 sec)
t2插入数据,发现跟上面不一样的是,t2阻塞了。
这时在t1中插入数据,成功插入:
mysql> insert into amount values('C',1000); Query OK, 1 row affected (0.02 sec)
当t1提交之后,此次轮到t2出现了主键重复的错误。
从结果能够知道,t1的事务并无受到t2事务的扰乱,即在Serializable的隔离级别下没有出现幻读。
在上面两个实验中咱们发现,repeatable read是没法避免幻读的,可是,在某种状况下,它却能解决幻读问题。
下面看例子1,查询的记录不存在的状况
t1 | t2 |
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
start transaction; | start transaction; |
mysql> select * from amount where id = 'E' for update; Empty set (0.00 sec) |
|
mysql> select * from amount; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | +----+-------+ 4 rows in set (0.00 sec) |
|
![]() 锁住了。 |
|
mysql> insert into amount values('E',1000); Query OK, 1 row affected (0.01 sec) |
|
commit | |
mysql> insert into amount values('E',1000); ERROR 1062 (23000): Duplicate entry 'E' for key 'PRIMARY' |
|
commit; |
使用select .. for update锁住,而后再insert,能够避免幻读。
其实,即便在t2中,插入id不为"E"的记录,也是会阻塞的(锁住),依然要等待t1提交后才能轮到t2工做。
例2 ,当查询的结果已经存在
t1 | t2 |
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
start transaction; | start transaction; |
mysql> select * from amount; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | +----+-------+ |
mysql> select * from amount; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | +----+-------+ |
mysql> select * from amount where id = 'A' for update; +----+-------+ | id | money | +----+-------+ | A | 100 | +----+-------+ |
|
mysql> select * from amount; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | +----+-------+ |
|
mysql> insert into amount values('C',1000); Query OK, 1 row affected (0.01 sec) |
|
commit; | commit; |
以上例子说明,for update时候,id为主键,RR策略时候,锁住了的条件符合的行,可是若是条件找不到任何列,锁住的是整个表,所以当t1查询到的记录为空时,在t2想插入该主键记录时是阻塞的;当t1查询到的记录非空时,除了该主键记录以外,能够在其余事务插入任何不存在的主键记录而不阻塞。
例3,范围查询的状况
t1 | t2 |
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
start transaction; | start transaction; |
mysql> select * from amount where id < 'M' for update; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | +----+-------+ |
|
mysql> insert into amount values('X',1000); Query OK, 1 row affected (0.01 sec) |
|
mysql> select * from amount ; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | | M | 1000 | | N | 1000 | +----+-------+
|
|
![]() 锁住了 |
|
mysql> select * from amount ; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | | M | 1000 | | N | 1000 | +----+-------+
|
|
commit; | |
mysql> select * from amount ; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | | M | 1000 | | N | 1000 | +----+-------+ 可重复读 |
能够看到,用 id<'M' 加的锁,只锁住了 id< 'M' 的范围,能够成功添加id为X的记录,添加id为'G'的记录时就会等待锁的释放。
例4
t1 | t2 |
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
start transaction; | start transaction; |
mysql> select * from amount; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | +----+-------+
|
|
mysql> insert into amount values('D',1000); Query OK, 1 row affected (0.01 sec)
|
|
mysql> select * from amount; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | +----+-------+ 可重复读 |
|
mysql> select * from amount lock in share mode; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | +----+-------+ 加锁,读取的是最新值,当前读 |
|
mysql> select * from amount for update; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | +----+-------+ 加锁,读取的是最新值,当前读 |
若是使用普通的读,会获得一致性的结果,若是使用了加锁的读,就会读到“最新的”“提交”读的结果。
自己,可重复读和提交读是矛盾的。在同一个事务里,若是保证了可重复读,就会看不到其余事务的提交,违背了提交读;若是保证了提交读,就会致使先后两次读到的结果不一致,违背了可重复读。
能够这么讲,InnoDB提供了这样的机制,在默认的可重复读的隔离级别里,能够使用加锁读去查询最新的数据。
结论:
MySQL InnoDB的可重复读并不保证避免幻读,须要应用使用加锁读来保证。而这个加锁度使用到的机制就是next-key locks。
mysql 的重复读解决了幻读的现象,可是须要 加上