- 什么是 MVCC
- Mysql InnoDB下的当前读和快照读
- 当前读
- 快照读
- MVCC 的实现原理
- 版本链
- trx_id
- roll_pointer
- 总结
- ReadView
- ReadView的四个重要内容
- 如何判断版本可见
- 读已提交和可重复读生成ReadView的时机
- 二次索引与MVCC
- 可重复读是否能解决幻读问题?
MVCC, 即多版本并发控制。MVCC 的实现,是通过**保存数据在某个时间点的快照来实现的**,提高了数据库的读写性能,以下文章都是围绕 InnoDB来说的,因为mylsam不支持事务
根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。
同一行数据平时发生读写请求时,会上锁阻塞,但mvcc用更好的方式去处理读写请求,做到在发生读写请求冲突时不用加锁。
读指的是快照读,而不是当前读,当前读是一种加锁的操作,是悲观锁
读取数据库记录,读取当前最新版本,会对当前读取数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作
如下操作都是当前读:
- select lock in share mode(共享锁)
- select for update (排他锁)
- update(排他锁)
- insert(排他锁)
- delete(排他锁)
- 串行化事务隔离级别
快照读是基于多版本并发控制,即mvcc,既然是多版本,那么快照读到的数据不一定是当前的最新数据,可能是之前历史版本的数据
MVCC 的实现原理对于 InnoDB ,聚簇索引记录中包含 3 个隐藏的列:
- ROW ID:隐藏的自增 ID,如果表没有主键,InnoDB 会自动按 ROW ID 产生一个聚集索引树。
- 事务 ID:记录最后一次修改该记录的事务 ID。
- 回滚指针:指向这条记录的上一个版本。
我们拿上面的例子,对应解释下 MVCC 的实现原理,如下图:
版本链每次更新记录,旧值放到undo日志,根据roll_pointer连成一条版本链,头节点是当前记录的最新值。另外还包含每个版本对应的事务id。
InnoDB聚簇索引的两个必要隐藏列:trx_id和roll_pointer,解释如下
trx_id一个事务每次对某条聚簇索引改动时,会把改事务的id赋值给trx_id。
roll_pointer每次对某条聚簇索引改动时,会将旧版本写到undo日志中。这个隐藏列相当于一个指针,可以通过它找到该记录修改前的信息。(Insert操作的undo日志没有该属性,insert undo只在事务回滚时发挥作用,事务提交后就没用了。
通过版本链来控制并发事务访问相同记录时的行为,称这种机制为多版本并发控制。
总结MVCC 实现的原理大致是,InnoDB 每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在 undo log 中。如果要执行更新操作,会将原记录放入 undo log 中,并通过隐藏的回滚指针指向 undo log 中的原记录。其它事务此时需要查询时,就是查询 undo log 中这行数据的最后一个历史版本。
MVCC 最大的好处是读不加锁,读写不冲突,极大地增加了 MySQL 的并发性。通过 MVCC,保证了事务 ACID 中的 I(隔离性)特性。
ReadView读未提交 直接读最新的版本就好了,但是 读已提交 和 可重复读 都必须保证读到已提交的事务修改过的记录,即另一个事务已经修改了记录但是未提交,则不能读取最新版的记录。
ReadView的四个重要内容核心问题:需要判断版本链中的哪个版本是当前事务可见的,通俗来说,就是判断版本链中要去选择哪个版本
当执行查询sql时会生成一致性视图read-view,查询的数据结果需要跟read-view做对比从而得到快照结果。
m_ids:在生成ReadView时,当前系统活跃读写事务的事务id列表(没有commit)。
min_trx_id:在生成ReadView时,当前系统活跃读写事务的最小事务id;也就是m_ids中的最小值。
max_trx_id:在生成ReadView时,系统应该分配给下一个事务的id值。
creator_trx_id:生成该ReadView的事务的事务id。只有执行insert、update、delete操作时才会分配事务id,否则该值默认为0。
如何判断版本可见举个栗子: 如果 m_ids中包含1、2、3,那么min_trx_id为1,creator_trx_id为4
访问版本的 trx_id 与creator_trx_id相同(事务访问自己的版本),可以访问该版本。
访问版本的 trx_id 小于min_trx_id,表明访问版本的事务已经commit了,在当前事务生成ReadView之前,是可以访问的
访问版本的trx_id大于max_trx_id,读取事务的时候,我们只能读取版本链中的数据,访问版本的事务id已经超出了版本链,所以不可访问
访问版本trx_id在min_trx_id和max_trx_id之间,如果等于m_ids中的一个值,是不可以访问的,反之可以(举个栗子:m_ids是1 3 4 5,如果trx_id为2,可以访问;为3,不能访问)
读已提交(解决脏读):每次读取数据(select)前都生成一个ReadView。
可重复读(解决脏读和不可重复读,幻读能否解决是不确定的):是以一个事务为单位,只在第一次读取(select)数据时生成ReadView。
不可重复读:事务A第一次查询id=1的name=’ 张三 ‘,过了一段时间后,事务A第二次查询id=1的name=’ 张大三 ‘,那是因为在这段时间内,事务B更新(update)了id=1的name=’ 张大三 ,事务B提交,由此事务A插查询到前后两次的数据不一致
视频:MySQL脏读、幻读、不可重复读你能分清吗?
二次索引与MVCC只有在聚簇索引中才有 trx_id 和 roll_pointer 隐藏列,那么二级索引(非聚簇索引)如何判断可见性呢?
二级索引页面的Page Header部分,有一个属性 PAGE_MAX_TRX_ID,执行增删改查时,如果执行操作的事务id大于PAGE_MAX_TRX_ID的值,则将其值设置为执行操作的事务id。即 PAGE_MAX_TRX_ID 代表修改该二级索引页面最大的事务id。
执行select时,如果ReadView的 min_trx_id 大于 PAGE_MAX_TRX_ID,则说明该页面的值对ReadView可见;如果小于,则需要回表进行判断。
利用二级索引中的主键进行回表操作,得到聚簇索引的记录后,再按照聚簇索引的方式从第一个版本开始,依次判断可见性。
mysql的可重复读在MVCC和锁机制下尽可能保证幻读问题,但是并不能完全禁止幻读。
特殊情况下,仍然可能出现幻读问题:
T1执行select生成ReadView
T2新插入一条记录,并且提交
ReadView不能阻止T1执行UPDATE或者DELETE语句来改动这条新插入的记录。(由于T2提交,改动这条记录并不能造成阻塞)
T1修改这条记录,这条记录的trx_id变成了T1的id。
之后T1再次查询时,在查询结果中就可以发现这条记录了



