zoukankan      html  css  js  c++  java
  • Mysql加锁过程详解(2)-关于mysql 幻读理解

    这里给出 mysql 幻读的比较形象的场景:

    users: id 主键

    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 的记录,没有则插入,这是我们期望的正常业务逻辑。

    T2 :干扰事务,目的在于扰乱 T1 的正常的事务执行。

    在 RR 隔离级别下,1、2 是会正常执行的,3 则会报错主键冲突,对于 T1 的业务来说是执行失败的,这里 T1 就是发生了幻读,因为T1读取的数据状态并不能支持他的下一步的业务,见鬼了一样。

    在 Serializable 隔离级别下,1 执行时是会隐式的添加 gap 共享锁的,从而 2 会被阻塞,3 会正常执行,对于 T1 来说业务是正确的,成功的扼杀了扰乱业务的T2,对于T1来说他读取的状态是可以拿来支持业务的。

    所以 mysql 的幻读并非什么读取两次返回结果集不同,而是事务在插入事先检测不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测读获取到的数据如同鬼影一般。

    这里要灵活的理解读取的意思,第一次select是读取,第二次的 insert 其实也属于隐式的读取,只不过是在 mysql 的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入。

    不可重复读侧重表达 读-读,幻读则是说 读-写,用写来证实读的是鬼影。

    下面给出幻读的例子:

    设置隔离级别为 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> commit;
    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 的重复读解决了幻读的现象,但是需要 加上 select for update/lock in share mode 变成当前读避免幻读,普通读select存在幻读

  • 相关阅读:
    Educational Codeforces Round 83 --- F. AND Segments
    Educational Codeforces Round 83 --- G. Autocompletion
    SEERC 2019 A.Max or Min
    2019-2020 ICPC Southwestern European Regional Programming Contest(Gym 102501)
    Educational Codeforces Round 78 --- F. Cards
    今天我学习了一门全新的语言
    codeforces 1323D 题解(数学)
    Educational Codeforces Round 80 (Div. 2) 题解 1288A 1288B 1288C 1288D 1288E
    Educational Codeforces Round 81 (Div. 2) 题解 1295A 1295B 1295C 1295D 1295E 1295F
    Codeforces Round #617 (Div. 3) 题解 1296C 1296D 1296E 1296F
  • 原文地址:https://www.cnblogs.com/yn-huang/p/11019711.html
Copyright © 2011-2022 走看看