- 0. 知识前要:
- 1. 举个栗子:
- 1.1. 栗子一:两次快照读之间存在更新语句,更新其他事务已经更新过的数据
- 1.1.1. 执行过程分析:
- 1.1.2. MVCC分析:
- 1.2. 栗子二:两次快照读之间存在更新语句,更新其他事务insert的数据
- 1.3. 栗子三:两次快照读之间存在更新语句,要更新的数据不冲突
- 1.4. 栗子四:使用锁定读取
- 3. 小结:
0. 知识前要:
- 要分析这个问题,首先得清楚什么是MVCC,详细的可以查看看一遍就理解:MVCC原理详解
- 这里小结一下:
- MVCC 是多版本并发控制,主要实现读写冲突不阻塞,这个读指的是快照读
- 它的实现主要依赖三个隐式字段、undo 日志、Read View
- 每行记录除了自定义的字段外,还有隐式主键、事务 id、指向 undo 日志的指针
- undo 日志记录有原始数据和修改数据,是个记录链,链首是最新的修改
- Read View 是一致性读视图,用来判断记录的某个版本是否对当前事务可见
- m_ids:当前系统中那些活跃(未提交)的读写事务ID列表
- min_limit_id:最小的事务id
- max_limit_id:最大的事务id,即分配给下一个事务的id值
- creator_trx_id:当前的事务id
- 匹配规则如下:
- RR 级别都是复用同一个 Read View,RC 则是每次快照读生成一个
1. 举个栗子:
- 初始数据:
1.1. 栗子一:两次快照读之间存在更新语句,更新其他事务已经更新过的数据
- 事务B的更新语句提交之后,事务A对事务B已提交的数据再进行更新,则事务B已提交的数据对事务A是可见的
- 官方文档也是这么介绍的,innodb-consistent-read,但文档也没找到原因
·
- 本栗子事务A最后的查询语句结果mobile=‘BBB’,amount=2000
1.1.1. 执行过程分析:
- 存储引擎执行update语句的过程
- 先从缓存池Buffer Pool读取数据,没有数据则从磁盘文件加载到缓存池
- 更新时再缓存池修改,并将更新的数据记录到redo日志缓存池
- 事务提交时,redo日志缓冲池根据策略写入redo日志磁盘文件,还会将本次修改的数据写入binlog,最后给redo日志追加commit标记
·- 事务A执行update语句时,事务B已经提交,意味着更新时会先获取最新的数据(不管是缓缓存池还是数据库加载的)
1.1.2. MVCC分析:
- 事务A第一次查询生成的视图快照,按照Read View的可见性判断规则,对比Undo版本链的记录
- 对比版本链最新的数据
min_limit_id =<trx_id(100)< max_limit_id
m_ids
包含版本记录的trx_id(100)
,且trx_id(100) = creator_trx_id
- 所以第二次查询语句查询到的就是mobile=‘BBB’,amount=2000的数据
1.2. 栗子二:两次快照读之间存在更新语句,更新其他事务insert的数据
- 事务A更新的数据是事务B新插入的数据,存在数据冲突
- 和上述栗子一一样,此时事务B新插入的数据对事务A是可见的
- 更新的时候会去取最新的数据取更新
- 所以最终查询结果会看到id = 2,4 的数据,并且id=4 的amout = 2000;
1.3. 栗子三:两次快照读之间存在更新语句,要更新的数据不冲突
- 和上面栗子一类似,只不过事务A更新的是id=5的数据
- 事务B更新提交id=1的数据,事务A再更新id=5的数据,更新的数据与事务B提交的不冲突
- 所以事务B提交的数据对事务A是不可见的
- 最后的查询结果还是mobie = ‘AAA’,amount = 1000
1.4. 栗子四:使用锁定读取
- 事务A第二条查询语句结果:mobie = ‘BBB’,amount = 1000
- 事务A第二条查询语句使用
FOR UPDATE 、FOR SHARE( LOCK IN SHARE MODE)
这类锁定读取(也属于当前读)- 从结果层面来看,猜测这类锁定读取不会使用版本链记录取匹配,而是直接获取的最新数据
3. 小结:
- RR 级别通过 MVCC 保证快照读不会出现幻读,通过临建锁保证当前读不会出现幻读;
- 但不能保证先快照读,再当前读的情况,所以严格意义上不算完全解决幻读
- 如果两次查询之间出现Update、Delete语句这类当前读,并且操作的语句数据与别的事务提交的数据冲突,则DML对当前事务可见
- 如果是锁定读取,会直接读取最新数据