vlambda博客
学习文章列表

Mysql的MVCC机制,学废了吗

小闭环,正反馈,持续输出第 4/100 天 | 深度 | 实践

前言

大概两年前因为排查一个数据可见性的问题也了解过 Mysql 的 MVCC,有兴趣的话可以看下这篇文章 。

当时觉得自己都懂了,但始终没有将这块知识串联起来,所以总感觉印象不深刻,其实本质上还是因为没有搞清楚这个东西的来龙去脉,没有真正地理解它。

现在我也没法说都懂了,只能说比以前的我更进步了些。

本文主要包含以下内容:

事务常见的并发问题事务隔离级别,在不同程度上解决了并发问题传统事务隔离级别,基于锁实现并发控制MVCC 无锁实现并发控制幻读

并发问题

多个事务并发执行,常见的问题如下,

读/读:没有数据改动,不存在并发问题。

写/写:如果两个事务同时读取同一条记录,并基于旧值对其进行修改,则会出现前一个事务的修改被后一个事务的修改覆盖的问题;或者前一个事务提交之后另一个事务发生回滚,出现已提交的修改被回滚操作覆盖的问题。这两类问题统称为丢失更新。

读/写或写/读,这种场景最复杂,可能出现如下问题:

一个事务读到另外一个事务未提交的修改,称为脏读一个事务中前后两次对同一记录的读取结果不一致,称为不可重复读一个事务中前后两次使用同样的查询条件读取数据,期间其它事务往表里插入了新数据,导致前后两次查询到的结果集不一致,称为幻读

将这些并发问题根据严重程度由高至低排序。

丢失更新 > 脏读 > 不可重复度 > 幻读

事务隔离级别

事务隔离级别在不同程度上解决了上述并发问题,保证事务本身特性(ACID),它是对并发问题的容忍性以及并发性能之间的权衡考量。

读未提交(RU),允许读取未提交的记录,会发生脏读、不可重复读、幻读问题读已提交(RC),只允许读物已提交的记录,解决了脏读问题,但会出现不可重复读、幻读问题可重复读(RR),不会发生脏读和不可重复读的问题,但会发生幻读问题串行化(S),事务串行执行,不存在并发问题,当然性能也最差

以上隔离级别,除串行化不会存在丢失更新的问题,其它几种隔离级别都存在该问题,需要通过对读操作额外加锁解决。

基于锁的实现

传统的事务隔离级别是基于锁实现的。

按照锁的互斥性,可以把锁分为:

共享锁(Shared Locks),简称 S锁排它锁(Exclusive Locks),简称 X锁

SS 不互斥,XS 互斥,XX 互斥,多个事务可以同时持有同一把 S锁。

按照锁的粒度,可以把锁分为:

行锁,只锁定某一行记录,其他行不受影响表锁,锁住整个表

按照锁的持有时间,可以把锁分为:

短锁,操作完成后立刻被释放长锁,直到事务提交后才被释放

四种隔离级别,对应的实现方式如下,

读未提交:读操作不加锁,写操作加上X锁直到事务提交后释放。

读提交:读操作加上S锁(且为短锁,操作完马上释放),写操作加上 X锁直到事务提交后释放。

可重复读:读操作加上S锁(且为长锁,事务提交后被释放),写操作加上 X锁直到事务提交后释放。

串行化:读写操作都加上X锁直到事务提交后释放,且锁的粒度为表锁。

可以在大脑里模拟下不同隔离级别的加锁场景,有助于加深对于并发控制的理解。

举个例子,读提交为什么能够解决脏读,因为它在读操作的时候需要加S锁,写操作需要加X锁,如果此时有其它未提交的事务修改了数据,它是没办法读取到数据的,需要等待其它事务提交或者回滚。

MVCC 实现

MVCC 英文全称 Multiversion Concurrency Control,即多版本并发控制,它通过无锁的方式实现了读提交,可重复读两种隔离级别下对于并发问题的控制,因此在系统性能方面也有显著的提升。

另外读未提交,串行化这两个隔离级别,不在 MVCC 的作用范围内,MVCC 侧重于优化读提交,可重复读隔离级别下读,写操作加锁互斥的性能问题。

MVCC 的核心实现主要包含两块内容:

多版本读视图

多版本

为了保证在不加锁的情况下,解决读、写操作并发问题,MVCC 引入了多版本的概念,即一个数据行同一时间可能存在多个版本。

在 InnerDB 引擎下,数据行都会包含两个重要的隐藏列:

事务ID(DB_TRX_ID):当事务对记录进行修改时,会把当前事务的事务ID记录到DB_TRX_ID中回滚指针(DB_ROLL_PTR):当事务对记录进行修改后,会把该记录的旧版本记录到undo日志中,通过回滚指针可以获取旧版本信息。

多个版本可以来源于不同事务,它们之间互不影响,这些版本按照先后顺序,被回滚指针被串联起来。

读视图

事务当前能够读取到哪个版本,则依赖读视图。

读视图本质上是一个数组,其中维护了一组事务id,例如[3, 4, 5, 6],

3 代表视图创建时存在的活跃事务中id最小值,称为 min_id6 代表未来要创建的事务id,称为 max_id其余的都是活跃事务id

事务在读取版本记录时,会拿该版本上的事务id与视图做比较:

如果该版本的事务id < min_id,则表示该版本在视图创建前已经提交,故该版本对当前事务可见如果该版本的事务id >= max_id,则表示该版本所属的事务在视图创建时还不存在,是未来创建的事务,故该版本对当前事务不可见如果 max_id > 该版本的事务id > min_id,则表示该版本所属的事务在视图创建时还未提交,故该版本对当前事务不可见当然如果该版本的事务id等于当前事务id,则认为该版本是由当前事务提交的,该版本对当前事务可见

如果发现符合条件的版本,直接返回;否则会通过回滚指针继续查找上一个版本的记录,直到将版本链遍历完还未找到符合条件的版本,则返回空。

读提交,可重复读两种隔离级别主要区别在于创建读视图的时机:

读提交:每次读取数据都会创建新的视图,因此前后两次读取的结果可能不一致,期间别的事务提交的内容也能读到可重复读:只有在事务开启后首次读取数据会创建视图

注意:由于 MVCC 是采用快照读的方式进行并发控制,读到的数据可能不是最新版本,如果业务上需要读取最新版本的数据,可以通过为读操作加锁强制当前读。

幻读

MVCC 无法直接解决幻读,只有串行化才能解决幻读,但串行化也带来极大的性能问题。

后来间隙锁出现了,于是出现了 MVCC + 间隙锁的解决方案,在避免幻读的同时,还能保证性能。

举个例子,查询某记录是否存在,不存在,准备插入此记录,但执行 插入时发现此记录已存在(其它事务插入了),导致无法插入,此时就发生了幻读。

可以通过如下方式为查询加锁:

select * from table where id = 1 for update;

如果 id = 1 的记录已经存在,则对该记录加上行锁;如果不存在,则会把 id = 1 这个间隙锁上,其它事务无法对该间隙进行插入操作,故解决了幻读问题,当然实际上间隙锁要复杂得多。

通过这次对 MVCC 相关知识点的梳理串联,让自己发现了不少之前忽略的问题,也对 MYSQL 的并发控制机制有了更系统的认识。




推荐阅读