事物的ACID概念应该大家都懂, 它的隔离级别以及并发问题也耳熟能详, 网上有很多非常非常好的比喻和描述
我就简单回顾一下吧, 用大白话的方式
我之前觉得好像就这么回事儿吧, 但是凌晨的时候读到一篇关于"MVCC究竟能否解决幻读"的文章, 秉着好奇心读了读, 发现之前的理解属实有点浅薄
就是一个其实挺明显的问题: 既然串行化是低性能的, 而可重复读是有幻读这个风险的, 难道就放任这样一个尴尬状态在并发环境中吗? 当时学习事务隔离级别及其并发问题的时候, 我有想到这个问题, 但只是觉得"可能这个问题是允许发生的吧…", 就没有深凿, 看来学习观念属实还需要提高啊
MVCC(Multi-Version Concurrency Control, 多版本并发控制), 具体细节挺多的, 可以去搜一下看看
简单来说, 是一种解决了乐观锁ABA问题的机制, 通过版本号(三个额外的隐藏version值)来实现
(乐观锁ABA问题如果不知道的话可以去搜一下, 5分钟肯定能明白, 我这儿就不多说这一段了. 乐观锁的思想在ConcurrentHashMap中也有体现, put()方法的CAS+synchronized)
不过有一个重点, 网上的大多数说法是"通过两个额外的隐藏version值来实现多版本", 但官网是这么说的, 注意红字部分:
链接在这: https://dev.mysql.com/doc/refman/5.6/en/innodb-multi-versioning.html
接着我们的内容
InnoDB默认隔离级别是Repeatable Read(也即MySQL的默认事务隔离级别), 解决了不可重复读的问题, 是通过MVCC来实现的. 但数据库中读的方式有两种, 快照读和当前读
(注: InnoDB是一种表级别的存储引擎, 还有一个叫MyISAM, 两者常常拿来对比, 它们的底层数据结构也很有学问, 如果要理解清楚的话, 至少N小时吧, 因人而异)
快照读, 顾名思义, 像冲相片一样读数据, 执行普通select语句的时候都是快照读
事务A在第一次select以后, InnoDB会默认执行一个快照读(类似于"拍一张照片"), 之后事务A的select都会根据这个快照来返回数据, 这两个select返回的是一样的数据, 符合Repeatable Read, 就相当于"一张拍好的照片, 我怎么看都是一样的"
但是呢, 如果两个普通的select期间有事务B执行insert, 这张"照片"就不新了. 不过由于快照读的存在, 事务A并没有感知到, 只要不触发当前读, 事务A都不会感知到. 在这个情况下, 快照读是没有导致幻读的, 甚至是"防止"了, 只不过没有读到最新插入的数据(关于读不到最新插入的数据这个情况, 我还暂时没有找到好的描述, 可能也还需要再看看). 但是, 如果有其它操作触发了当前读, 就不一样了
当前读, 听起来比较没那么直白, 不过举几个实例就直白了, 它是由一些特定的操作所触发的:
至于为什么这些操作会触发当前读呢? 可以联想一下Java中synchronized和volatile的区别, 有很多共性!
接着上面事务A和B的场景. 事务A已经select过了, 事务B也已经insert过了, 但事务A丝毫不知道事务B介入了这些数据. 如果此时事务A执行update, 再select, 就会触发当前读, 刚刚事务B所insert的数据就会一起出现. 但对于事务A来说, 它并没有执行过这个新出现数据的操作(因为是由事务B插入的, 事务A并不知道), 很懵, 就像幻觉一样, 即幻读了
举一个我在网上看到的例子:
所以, MVCC并不能根本上解决幻读的情况. 准确地说, 在"快照读"的场景下不会引起幻读, 但是在"当前读"场景下会引起幻读
那么怎么办呢? 先讲一个相关的概念吧, InnoDB存储引擎有三个锁算法:
举个例子就比较清晰了:
假如A事务正在查询id<10的所有数据, 只存在id为1~7的数据, 8、9并不存在. 此时B事务向数据库插入id为8的数据, 那么事务A即将在下一次"当前读"时出现幻读, 对吧?
这个时候, Next-Key Lock就派上用场了. 它相当于Record+Gap两个锁, 而Record Lock(行锁)会锁着1~7(已有数据)的位置, Gap Lock会锁着id为8、9的两个现在没有数据的位置. 这种情况下, 事务A读取数据的时候, 事务B向数据库插入数据(无论是1,2,3,4,5,6,7还是8,9)都会被阻止, 即防止了幻读
综上所述, 幻读的解决实际上有两种方式, 但MVCC解决幻读的这个情况我觉得有点"擦边球", Next-Key Lock这个解决方案是更加直接的
有了Next-Key Lock这个解决方案, Serializable这个低效率的事务隔离级别就可以不去使用了, 兼顾了实现和性能, 属实厉害
顺口一提, 这其实很像在ConcurrentHashMap中使用volatile在get()方法中替代synchronized的设计思想, 都是并发访问的轻量级实现方式, 都在实现功能的前提下大幅提高了性能, 可能这就是计算机科学吧
以上内容如有错误, 恳请指正. 有时间再更