介绍一下 MVCC
分析
这部分内容要求从以下三个方面回答:
MVCC是什么?
解决了什么问题?
MVCC实现原理?
MVCC(多版本并发控制)是一种并发控制机制、通过为每个事务创建数据快照、使得事务读取数据时只能看到在其开始之前已提交的数据版、而不是最新的数据。
核心思想是通过维护数据的多个版本来实现读写操作的并发执行去实现读写操 然后不会相互阻塞
读操作不加锁、写操作只需创建新版本。
读操作不会加锁、而是读取符合其版本可见性的旧数据版本、因此不会阻塞写操作。
写操作不会覆盖原数据、而是创建新的版本、旧版本仍然保留供其他事务读取
MVCC 解决了读写阻塞、脏读和不可重复读等问题
MVCC 的实现原理:
(基于 InnoDB 引擎)下的版本链机制和 ReadView 机制
首先他有版本链机制:
每次修改数据时:都会创建一个新的版本。并将旧版本保存在 Undo Log 中。
Undo Log会形成一个版本链:
每个数据行都包含以下隐藏字段:
DB_TRX_ID
:创建或最后修改该行版本的事务 IDDB_ROLL_PTR
:回滚指针、指向 Undo Log 中该行记录的前一个版本DB_ROW_ID
:行 ID(仅在表没有主键时存在)
通过 回滚 指针、可以从当前版本追溯到历史版本,形成一个完整的版本链:
还有ReadView 机制
ReadView 生成时机是
读已提交(RC):每次快照读都生成新的 ReadView
可重复读(RR):事务内第一次快照读时生成 ReadView、后续复用
ReadView 包含以下关键信息:
trx_ids
:当前活跃事务列表
up_limit_id:
最小活跃事务ID
low_limit_id:
下一个要分配的事务ID
creator_trx_id:
创建ReadView的事务ID
在读取数据时 InnoDB 会根据 ReadView 和版本链中的 row trx_id
判断哪个版本的数据对当前事务是可见的
可见行判断规则:根据数据版本的 DB_TRX_ID
进行判断:
小于 最小活跃事务ID:则该版本对当前事务可见
大于下一个要分配的事务ID:不可见
在两者之间:检查是否在活跃事务列表中、在则不可见、不在则可见
如果当前版本不可见、就沿着版本链查找可见版本
什么是幻读?
幻读是指在同一个事务中、相同条件的查询前后两次返回的记录数不一致,因为有其他事务插入了满足条件的新记录。
幻读是当前读引起的
RR级别的幻读是当前读还是快照读 ->字节面过->>>答案是这是当前读
快照读不会发生幻读
快照读使用 MVCC、基于事务开始时的 ReadView
即使其他事务插入了新数据并提交、当前事务的快照读仍然看不到这些新数据
所以快照读天然避免了幻读问题
当前读场景下会出现幻读
当前读(SELECT ... FOR UPDATE、UPDATE、DELETE 等)会读取最新提交的数据
不使用 MVCC 机制、而是直接读取当前最新版本
这时如果其他事务插入了新数据,当前读就能看到、造成幻读
MVCC 在不同隔离级别下的行为不同:
事务有快照读和当前读 我们来基于这两种去思考:
在读已提交(RC)隔离级别下 、每次快照读都会生成新的 ReadView。解决了脏读
MySQL 的 InnoDB 引擎、默认隔离级别是可重复读(RR)、在这个级别下:
事务内首次快照读 也就是第一次快照读的时候生成 ReadView、后续复用。也就是对于普通的 SELECT 查询、也就是快照读的操作、使用MVCC 解决了快照读场景下的脏读、不可重复读。
不同的地方在于:
RC每次快照读都会生成新的ReadView
RR只有第一次快照读的时候会生成ReadView 后续会复用
RC 隔离级别的目标是:每次查询都能看到最新提交的数据、所以它需要为每次快照读重新构建视图
RR 隔离级别的目标是:在整个事务期间、保证相同查询条件结果一致、所以只生成一次 ReadView、确保版本一致性
事务还有当前读的操作:
为什么当前读会出现幻读问题呢?为什么 MVCC 不能彻底解决幻读?
当前读指的是对记录加锁的读操作(如:SELECT ... FOR UPDATE、UPDATE、DELETE 等)。
当前读可能会发现幻读问题
幻读是指在同一事务中、使用相同的查询条件、多次查询的结果集不一致、主要是由于其他事务插入了新的数据
因为MVCC 的作用范围是行级别并发控制:
它是基于 版本链(Undo Log) 实现的、核心思想是通过快照判断某一行是否对当前事务可见
也就是说,MVCC 控制的是已经存在的行的版本可见性
但幻读的问题是:
新插入的整行记录本身就不在原快照中、MVCC 根本不掌握该行的历史。
比如另一个事务新增一行满足查询条件、当前事务在原快照中根本找不到它、这就构成了幻读
所以说MVCC不能完全避免幻读
MySQL的可重复读隔离级别并没有彻底解决幻读问题、只是很大程度上避免了幻读现象的发生
当前读下的幻读:如果事务开启后、先执行快照读、然后其他事务插入了新记录 、然后事务使用当前读查询时可能会发现两次查询结果不同 从而发生幻读
然后为了避免幻读
避免幻读的方式就是
在事务开启后、立即执行
SELECT ... FOR UPDATE
这类当前读语句。这样做会对记录加
next-key lock
、从而避免其他事务插入新记录、减少幻读的发生。MySQL 的InnoDB 通过 间隙锁(Gap Lock) 和 临键锁(Next-Key Lock) 来解决幻读
Next-Key Lock 是间隙锁和记录锁的组合、对记录加锁、防止其他事务修改该记录。防止其他事务在当前事务的查询范围内插入新的数据、从而避免幻读。解决了当前读场景下的幻读问题