【2025最新Java面试八股】如何理解MySQL的MVCC机制?

发布于:2025-04-23 ⋅ 阅读:(18) ⋅ 点赞:(0)

面试回答要点

当面试官问及MySQL的MVCC机制时,可以这样组织回答:

"MVCC(Multi-Version Concurrency Control,多版本并发控制)是MySQL实现高并发事务的一种重要机制,它通过保存数据在某个时间点的快照来实现非阻塞读操作。InnoDB存储引擎通过undo日志和版本链来实现MVCC,使得读操作不需要等待写操作完成,写操作也不需要等待读操作完成,从而提高了数据库的并发性能。"

所谓快照读,就是读取的是快照数据,即快照生成的那一刻的数据,像我们常用的普通的SELECT语句在不加锁情况下就是快照读。

当前读就是读取最新数据,所以,加锁的 SELECT,或者对数据进行增删改都会进行当前读

在MySQL 中,只有READ COMMITTED 和 REPEATABLE READ这两种事务隔离级别才会使用快照读。

●在 RR 中,快照会在事务开始时生成,只有在本事务中对数据进行更改才会更新快照。
●在 RC 中,每次读取都会重新生成一个快照,总是读取行的最新版本。

在InnoDB中,MVCC就是通过Read View + Undo Log来实现的,undo log中保存了历史快照,而Read View 用来判断具体哪一个快照是可见的。

MVCC核心原理

1. 版本链与隐藏字段

InnoDB中每行记录都包含几个隐藏字段:

  • DB_TRX_ID:最近修改该行记录的事务ID

  • DB_ROLL_PTR:指向undo日志中旧版本数据的指针

  • DB_ROW_ID:行ID(当没有主键时自动生成)

在undo log中,因为每一次记录变更之前都会先存储一份快照到undo log中,那么这几个隐式字段也会跟着记录一起保存在undo log中,就这样,每一个快照中都有一个db_trx_id字段表示了对这个记录做了最新一次修改的事务的ID ,以及一个db_roll_ptr字段指向了上一个快照的地址。(db_trx_id和db_roll_ptr是重点,后面还会用到)

这样,就形成了一个快照链表:

2. ReadView机制

Read View 主要来帮我们解决可见性的问题的, 即他会来告诉我们本次事务应该看到哪个快照,不应该看到哪个快照。

在可重复读(REPEATABLE READ)和读已提交(READ COMMITTED)隔离级别下,事务在第一次执行SELECT时会生成一个ReadView,包含:

  • m_ids:当前活跃(未提交)的事务ID列表

  • min_trx_id:m_ids中的最小值

  • max_trx_id:系统预分配的下一个事务ID

  • creator_trx_id:创建该ReadView的事务ID

假设当前事务要读取某一个记录行,该记录行的 db_trx_id(即最新修改该行的事务ID)为 trx_id,那么,就有以下几种情况了:

●1、trx_id< up_limit_id,即小于5的事务,说明这些事务在生成ReadView之前就已经提交了,那么该事务的结果就是可见的。

●2、trx_id>low_limit_id,即大于8的事务,说明该事务在生成 ReadView 后才生成,所以该事务的结果就是不可见的。

●3、up_limit_id<trx_id<low_limit_id,即大于等于5,小于8,这种情况下,会再拿事务ID和Read View中的trx_ids进行逐一比较。
○如果,事务ID在trx_ids列表中,那么表示在当前事务开启时,这个事务还是活跃的,那么这个记录对于当前事务来说应该是不可见的。
○如果,事务id不在trx_ids列表中,那么表示的是在当前事务开启之前,其他事务对数据进行修改并提交了,所以,这条记录对当前事务就应该是可见的。
○当然这里有个例外情况,那就是这个trx_id=creator_trx_id,那么就肯定是可见的

所以,当读取一条记录的时候,经过以上判断,发现记录对当前事务可见,那么就直接返回就行了。那么如果不可见怎么办?没错,那就需要用到undo log了。

当数据的事务ID不符合Read View规则时候,那就需要从undo log里面获取数据的历史快照,然后数据快照的事务ID再来和Read View进行可见性比较,如果找到一条快照,则返回,找不到则返回空。

3. 可见性判断规则

通过比较版本链中记录的DB_TRX_ID与ReadView来判断数据是否可见:

  1. 如果DB_TRX_ID == creator_trx_id,说明是当前事务修改的,可见

  2. 如果DB_TRX_ID < min_trx_id,说明在ReadView创建前已提交,可见

  3. 如果DB_TRX_ID >= max_trx_id,说明在ReadView创建后开启的事务,不可见

  4. 如果min_trx_id <= DB_TRX_ID < max_trx_id,且不在m_ids中,说明已提交,可见

4. 不同隔离级别的差异

  • 读已提交(READ COMMITTED):每次SELECT都会生成新的ReadView

  • 可重复读(REPEATABLE READ):只在第一次SELECT时生成ReadView

MVCC的优势

  1. 读不阻塞写,写不阻塞读:提高了并发性能

  2. 减少锁的使用:多数读操作不需要加锁

是否解决了幻读问题

InnoDB中的REPEATABLE READ这种隔离级别通过间隙锁+MVCC解决了大部分的幻读问题,但是并不是所有的幻读都能解读,想要彻底解决幻读,需要使用Serializable的隔离级别。

RR中,通过间隙锁解决了部分当前读的幻读问题,通过增加间隙锁将记录之间的间隙锁住,避免新的数据插入。但是必须要及时加间隙锁才可以有效解决幻读,如果在加间隙锁之前发生了增删改操作就会发生幻读。举个例子

两个事务,事务1先进行快照读,然后事务2插入了一条记录并提交,再在事务1中进行update新插入的这条记录是可以更新成功的,这就是发生了幻读。
两个事务,事务1先进行快照读,然后事务2插入了一条记录并提交,在事务1中进行了当前读之后,再进行快照读也会发生幻读。

在上面的例子中,在事务1中,假设我们并没有在事务开启后立即加锁,而是进行了一次普通的查询,然后事务2插入数据成功之后,再通过事务1进行了2次查询。我们发现,事务1后面的两次查询结果完全不一样,没加锁的情况下,就是快照读,读到的数据就和第一次查询是一样的,就不会发生幻读。但是第二次查询加了锁,就是当前读,那么读取到的数据就有其他事务提交的数据了,就发生了幻读。

在RR的级别下,当我们使用SELECT … FOR UPDATE的时候,会进行加锁,不仅仅会对行记录进行加锁,还会对记录之间的间隙进行加锁,这就叫做间隙锁。因为记录之间的间隙被锁住了,所以事务2的插入操作就被阻塞了,一直到事务1把锁释放掉他才能执行成功。因为事务2无法插入数据成功,所以也就不会存在幻读的现象了。所以,在RR级别中,通过加入间隙锁的方式,就避免了幻读现象的发生。

RR中,通过MVCC机制的,解决了快照读的幻读问题,RR中的快照读只有第一次会进行数据查询,后面都是直接读取快照,所以不会发生幻读。


网站公告

今日签到

点亮在社区的每一天
去签到