2025年精通MVCC
今年找工作,无一例外又问到了MVCC这个知识点。几乎每次换工作都会被问到这个面试有用,工作毫无 * 用的知识。但是环境就是这样,既然如此,我们用一篇文章彻底搞懂MVCC
1.MVCC是什么
MVCC(Multi-Version Concurrency Control,多版本并发控制) 是数据库中常用的一种 并发控制机制,它通过为每个事务提供数据的多个版本,实现了 读写分离、读不加锁,大幅提升数据库并发性能。
也就是说,它是为了解决当出现多个线程访问数据库时出现的一些问题。那么首先先搞清楚有哪些问题,再说解决方案。
2解决了什么问题
2.1 用锁来实现事务的效率问题
如果没有MVCC,那么想实现事务只能加读锁和写锁了,首先写锁是肯定要加的,不能A操作一个数据时,B也能操作同一个数据吧。且A操作一个数据时,B也不能读,因为没有MVCC,你这时候去读就能读到A操作的中间态数据。就像五我才写了一横,你就直接读了,你以为我写的是一。既然事务是必须要实现的,不管你是用锁,还是MVCC。那么我们来思考如何不用锁,也能实现事务。
这里提到事务的隔离级别:
READ UNCOMITTED:读未提交
READ COMMITTED:读提交
REPEATABLE READ:可重复读
SERIALIZABLE:串行化
上文提到,如果连锁都不用,属于读未提交,能读到其他事务的中间态(事务未提交的数据),那也没什么隔离可说了。MySQL既然是事务型数据库,读未提交就不需要讨论(某些业务场景需要获取到未提交的数据状态时,可以将隔离级别设为读未提交,比如输出一些状态日志。)。
串行化就等于加读写锁,没有MVCC又要保证事务那就是这个级别,但是效率又太低。那么这时候MVCC来了。它通过判断trx_id的大小决定哪些数据对我可见,那么别人写的时候,我就可以通过这个判定依据决定我可以看见哪个版本下的数据了,我就不会读到脏数据,也就不需要加锁,也就实现了多版本并发控制。
2.2 脏读问题
首先要明确,读到别人未提交的数据,就是脏读,那自然可以知道READ COMMITTED:读提交,这个隔离级别是可以阻止脏读的。那如何实现读提交隔离级别呢?
那么MVCC来了。
当事务 T 读取数据时,对于某一行记录的版本 trx_id
(事务ID):
-
如果
trx_id
< 当前Read View
中的最小活跃事务ID ⇒ 可见(早已提交) -
如果
trx_id
是正在进行中的事务 ⇒ 不可见 -
如果
trx_id
== 当前事务ID ⇒ 可见(自己写的可见) -
否则,根据
undo log
找前一版本,继续判断。
说人话就是,当你去查询某一行数据时,这一行数据有一个历史记录列表(undo log实现,且列表每一行都有一个trx_id)。你去对比这个历史记录列表,现在活跃事务id中有10个是活跃的,事务id是自增的,你找到最小的活跃事务id,然后你能看见的数据就是历史记录列表中trx_id小于最小活跃事务id的记录数据。也就是你只能看见已经提交,不处于活跃状态的数据。
也就是说,数据库是通过对比trx_id,来控制判断哪些已操作完成的数据你可以看见,正在操作的数据你不可以看见。
那么就引出了最常见的面试核心内容:
InnoDB 如何实现 MVCC?
InnoDB 在行级存储和事务控制的基础上,通过以下机制实现 MVCC:
1. 每行记录隐含两个字段:
字段名 含义 trx_id
最后修改该行的事务ID roll_pointer
(或称roll_ptr
)指向旧版本数据的指针(Undo Log) 2. 使用 Undo Log(回滚日志)保存历史版本
每次对数据的更新/删除操作都会记录旧版本到 Undo Log;
读取操作会根据当前事务的 Read View,从 Undo Log 中找到合适的版本供事务“读取”。
3. Read View(读取快照)
每个事务在执行
SELECT
时,会创建一个 Read View;这个视图记录了哪些事务“对我来说是不可见的”,以此决定读取哪个版本。
2.3 不可重复读
我们知道MVCC工作在:
READ COMMITTED:读提交
REPEATABLE READ:可重复读
两个级别下,READ COMMITTED是有不可重复读的问题的,同一个事务内,如果其他事务提交了数据,我是能感知到的。那么解决不可重复读问题,就是采用默认的REPEATABLE READ:可重复读隔离级别。就是采用同一个事务中,始终只访问第一次select时的Read View快照(读取快照)。
3 如何实现
那么记住核心的内容:
采用每一行记录trx_id来对比大小,判定可见区域。
采用roll_pointer来寻找上一条旧数据,链式寻找,也就是Undo Log一个最大的作用。
采用Read View读快照来保存历史数据。
最后,记住MVCC的实现,以及读提交,可重复读这两个隔离级别不能避免幻读问题。这个关于间隙锁,后面说。