美文网首页程序员
【纠错系列】不可重复读与幻读的区别

【纠错系列】不可重复读与幻读的区别

作者: 妖云小离 | 来源:发表于2019-10-12 19:10 被阅读0次

    纠错

    我猜,你们在各种博文中看到对于幻读的解释是这样的:

    一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
    即事务A 执行两次 select 操作得到不同的数据集,即 select 1 得到 10 条记录,select 2 得到 11 条记录。

    这其实并不是幻读,这是不可重复读的一种,只会在 R-U R-C 级别下出现,而在 mysql 默认的 RR 隔离级别是不会出现的(下面会举例推翻)。

    然而,我终于在茫茫文章中,找到了相对正确的解释:

    幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作
    更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。

    推翻错误的解释

    事务隔离级别

    mysql 有四级事务隔离级别 每个级别都有字符或数字编号

    读未提交 READ-UNCOMMITTED | 0:存在脏读,不可重复读,幻读的问题

    读已提交 READ-COMMITTED | 1:解决脏读的问题,存在不可重复读,幻读的问题

    可重复读 REPEATABLE-READ | 2:解决脏读,不可重复读的问题,存在幻读的问题,默认隔离级别,使用 MMVC机制 实现可重复读

    序列化 SERIALIZABLE | 3:解决脏读,不可重复读,幻读,可保证事务安全,但完全串行执行,性能最低

    幻读会在 RU / RC / RR 级别下出现,SERIALIZABLE 则杜绝了幻读,但 RU / RC 下还会存在脏读,不可重复读,故我们就以 RR 级别来研究幻读,排除其他干扰。

    举例推翻

    建表a,id列自增主键,name列唯一索引。

    CREATE TABLE a (
      id int(11) NOT NULL AUTO_INCREMENT,
      name varchar(255) DEFAULT NULL,
      PRIMARY KEY (id),
      UNIQUE KEY UIDX_NAME (name)
    ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
    

    准备初始数据:

    INSERT INTO a VALUES(NULL,'a'),(NULL,'b');
    

    MySQL隔离级别设置为RR(默认),准备两个事务AB。然后依次执行:

    1. 开始事务A,执行查询语句:
    START TRANSACTION;
    SELECT * FROM a WHERE name BETWEEN 'a' AND 'z';
    
    +----+------+
    | id | name |
    +----+------+
    |  1 | a    |
    |  2 | b    |
    +----+------+
    2 rows in set
    
    1. 开启事务B,执行插入语句,并提交
    START TRANSACTION;
    INSERT INTO a VALUES(NULL,'c'),(NULL,'d');
    COMMIT;
    
    1. 事务A再次执行查询语句:
    SELECT * FROM a WHERE name BETWEEN 'a' AND 'z';
    
    +----+------+
    | id | name |
    +----+------+
    |  1 | a    |
    |  2 | b    |
    +----+------+
    2 rows in set
    

    小结

    可以看到,在RR级别下,所谓的"幻读"并没有出现。而SQL-92标准中定义的RR级别是没法解决幻读的,这就是矛盾点所在。如果是所谓的"幻读",事务A应该读到abcd四条数据。出现这种情况有两种可能:

    1. MySQL在RR级别解决了幻读。
    2. 这不是真正的幻读。

    查阅MySQL官方文档,并没有某一段文字说明其在RR级别解决了幻读问题。

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

    所以说,其实这并不是“幻读”的范畴,这仍然属于RR级别所解决的,不可重复读范畴。

    解释

    我们能确定的是,RR级别解决了不可重复读的问题。
    那么为什么说上述例子属于不可重复读范畴呢?我们得从解决不可重复读问题的原理MVCC讲起。

    MVCC

    MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC。

    在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

    快照读VS当前读

    在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?以MySQL InnoDB为例:

    快照读:简单的select操作,属于快照读,不加锁。

    select * from table where ?;
    

    当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。

    select * from table where ? lock in share mode;
    select * from table where ? for update;
    insert into table values (…);
    update table set ? where ?;
    delete from table where ?;
    

    所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。

    为什么将 插入/更新/删除 操作,都归为当前读?

    一个Update操作的具体流程。当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁 (current read)。待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了一个当前读。同理,Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查,也会进行一个当前读。

    注:针对一条当前读的SQL语句,InnoDB与MySQL Server的交互,是一条一条进行的,因此,加锁也是一条一条进行的。先对一条满足条件的记录加锁,返回给MySQL Server,做一些DML操作;然后在读取下一条加锁,直至读取完毕。

    总结

    所以说,在上述例子中,事务A第二次读取(快照读)的是记录的历史版本。而不是最新的版本——事务B插入新纪录后的abcd四条。这属于不可重复读的范畴。

    有些博文错误的把这种情况归为“幻读”。甚至还有的说MySQL的RR级别解决了幻读。在此纠错。

    参考:

    https://segmentfault.com/a/1190000016566788?utm_source=tag-newest
    http://hedengcheng.com/?p=771

    相关文章

      网友评论

        本文标题:【纠错系列】不可重复读与幻读的区别

        本文链接:https://www.haomeiwen.com/subject/egfcmctx.html