MySQL-MVCC机制

一、前言

前面介绍了MySQL的锁机制,提到过写锁和读锁是冲突的,那么MySQL是如何处理读写冲突,提高数据库高并发场景下的吞吐性能?本章主要是介绍MVCC (Multiversion Concurrency Control) ,即多版本并发控制。

二、什么是MVCC

SELECT 操作可以不加锁而是通过 MVCC 机制读取指定的版本历史记录,并通过一些手段保证保证读取的记录值符合事务所处的隔离级别,从而解决并发场景下的读写冲突。

下面举一个多版本读的例子,例如两个事务 A 和 B 按照如下顺序进行更新和读取操作:

MySQL-MVCC机制_第1张图片
在事务 A 提交前后,事务 B 读取到的 x 的值是什么呢?答案是:事务 B 在不同的隔离级别下,读取到的值不一样。

  • 如果事务 B 的隔离级别是读未提交(RU),那么两次读取均读取到 x 的最新值,即 20。
  • 如果事务 B 的隔离级别是读已提交(RC),那么第一次读取到旧值 10,第二次因为事务 A 已经提交,则读取到新值 20。
  • 如果事务 B 的隔离级别是可重复读或者串行(RR,S),则两次均读到旧值 10,不论事务 A 是否已经提交。

可见在不同的隔离级别下,数据库通过 MVCC 和隔离级别,让事务之间并行操作遵循了某种规则,来保证单个事务内前后数据的一致性。即多版本并发控制。

三、MVCC作用

InnoDB 相比 MyISAM 有两大特点,一是支持事务而是支持行级锁,事务的引入带来了一些新的挑战。相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括以下几种情况:

  • 更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题 —— 最后的更新覆盖了其他事务所做的更新。如何避免这个问题呢,最好在一个事务对数据进行更改但还未提交时,其他事务不能访问修改同一个数据。
  • 脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些尚未提交的脏数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做 “脏读”。
  • 不可重复读(Non-Repeatable Reads):一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了!这种现象叫做“不可重复读”。
  • 幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为 “幻读”。

以上是并发事务过程中会存在的问题,解决更新丢失可以交给应用,但是后三者需要数据库提供事务间的隔离机制来解决。实现隔离机制的方法主要有两种:

  • 加读写锁;
  • 一致性快照读,即MVCC;

但本质上,隔离级别是一种在并发性能和并发产生的副作用间的妥协,通常数据库均倾向于采用 Weak Isolation。

InnoDB 中的 MVCC

  • MySQL中 InnoDB 引擎支持 MVCC;
  • 应对高并发事务, MVCC 比单纯的加行锁更有效, 开销更小;
  • MVCC 在读已提交(Read Committed)和可重复读(Repeatable Read)隔离级别下起作用;
  • MVCC 既可以基于乐观锁又可以基于悲观锁来实现;

四、MVCC实现原理

总体上来讲MVCC的实现是基于ReadView版本链以及Undo日志实现的。

4.1 版本链

对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL唯一键时都不会包含row_id列):

  • DATA_TRX_ID:记录最近更新这条行记录的事务 ID,大小为 6 个字节,每次对某条聚簇索引记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。
  • DATA_ROLL_PTR:表示指向该行回滚段(rollback segment)的指针,大小为 7 个字节,InnoDB 便是通过这个指针找到之前版本的数据。该行记录上所有旧版本,在 undo 中都通过链表的形式组织。每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
  • DB_ROW_ID:行标识(隐藏单调自增 ID),大小为 6 字节,如果表没有主键,InnoDB 会自动生成一个隐藏主键,因此会出现这个列。另外,每条记录的头信息(record header)里都有一个专门的 bit(deleted_flag)来表示当前记录是否已经被删除。

如何组织 Undo Log 链

上文提到,在多个事务并行操作某行数据的情况下,不同事务对该行数据的 UPDATE 会产生多个版本,然后通过回滚指针组织成一条 Undo Log 链,这节我们通过一个简单的例子来看一下 Undo Log 链是如何组织的,DATA_TRX_ID 和 DATA_ROLL_PTR 两个参数在其中又起到什么样的作用。

还是以上文 MVCC 的例子,事务 A 对值 x 进行更新之后,该行即产生一个新版本和旧版本。假设之前插入该行的事务 ID 为 100,事务 A 的 ID 为 200,该行的隐藏主键为 1。

MySQL-MVCC机制_第2张图片

事务 A 的操作过程为:

  • 对 DB_ROW_ID = 1 的这行记录加排他锁,把该行原本的值拷贝到 undo log 中,DB_TRX_ID 和 DB_ROLL_PTR 都不动,修改该行的值这时产生一个新版本,更新 DATA_TRX_ID 为修改记录的事务 ID,将DATA_ROLL_PTR 指向刚刚拷贝到 undo log 链中的旧版本记录,这样就能通过 DB_ROLL_PTR 找到这条记录的历史版本。如果对同一行记录执行连续的 UPDATE,Undo Log 会组成一个链表,遍历这个链表可以看到这条记录的变迁。
  • 记录 redo log,包括 undo log 中的修改,那么INSERT和DELETE会怎么做呢?其实相比 UPDATE 这二者很简单,INSERT 会产生一条新纪录,它的 DATA_TRX_ID 为当前插入记录的事务 ID;DELETE 某条记录时可看成是一种特殊的 UPDATE,其实是软删,真正执行删除操作会在 commit 时,DATA_TRX_ID 则记录下删除该记录的事务 ID。

4.2 ReadView

在 RU 隔离级别下,直接读取版本的最新记录就 OK,对于 SERIALIZABLE 隔离级别,则是通过加锁互斥来访问数据,因此不需要 MVCC 的帮助。因此 MVCC 运行在 RC 和 RR 这两个隔离级别下,当 InnoDB 隔离级别设置为二者其一时,在 SELECT 数据时就会用到版本链

核心问题是版本链中哪些版本对当前事务可见?
InnoDB 为了解决这个问题,设计了 ReadView(可读视图)的概念。

RR 下的 ReadView 生成

在 RR 隔离级别下,每个事务 touch first read 时(本质上就是执行第一个 SELECT 语句时,后续所有的 SELECT 都是复用这个 ReadView,其它 update, delete, insert 语句和一致性读 snapshot 的建立没有关系),会将当前系统中的所有的活跃事务拷贝到一个列表生成ReadView。

RC 下的 ReadView 生成

在 RC 隔离级别下,每个 SELECT 语句开始时,都会重新将当前系统中的所有的活跃事务拷贝到一个列表生成 ReadView。二者的区别就在于生成 ReadView 的时间点不同,一个是事务之后第一个 SELECT 语句开始、一个是事务中每条 SELECT 语句开始。

总结

RC、RR 两种隔离级别的事务在执行普通的读操作时,通过访问版本链的方法,使得事务间的读写操作得以并发执行,从而提升系统性能。RC、RR 这两个隔离级别的一个很大不同就是生成 ReadView 的时间点不同,RC 在每一次 SELECT 语句前都会生成一个 ReadView,事务期间会更新,因此在其他事务提交前后所得到的 m_ids 列表可能发生变化,使得先前不可见的版本后续又突然可见了。而 RR 只在事务的第一个 SELECT 语句时生成一个 ReadView,事务操作期间不更新。

五、扩展

5.1 当前读、快照读

当前读:

select...lock in share mode (就这个是共享锁)
select...for update
update , delete , insert

当前读, 就是加了锁,读取的是最新版本, 并且对读取的记录加锁, 不让其他事务同时去改(其他事务要阻塞下)。

快照读:

普通select时,基于提升并发的考虑,基于MVCC多版本并发控制(行锁的变种)改进成不加锁了(除非是隔离级别是串行),读的是视图, 不是最新数据,如果先select 再update 可能会看到幻行, 因为select读的可能是老的版本, 有其他事务插入或者删除过行,所以和更新读到的最新版本行数不一样,所以说InnoDB的RR隔离级别没有或者解决了幻读问题都不太准确。应该说它并没有完全解决幻读的问题。

  • 如果在同一个事务里面,只是总是执行普通的select快照读,是不会产生幻读的。
  • 但是如果在这个事务里面通过当前读或者先更新然后快照读的形式来读取数据,就会产生幻读。

本文转载整理于:https://zhuanlan.zhihu.com/p/64576887

你可能感兴趣的:(MySql)