Mysql InnoDB MVCC原理

一、概述

MVCC: Multi-Version Concurrency Control 多版本并发控制
本质上是一种行级锁的变种,在MySQL、PosgreSQL、Oracle中都有运用。MVCC可以由乐观锁或悲观锁来实现,事实上,不同的存储引擎的实现是不同的,但目的都是提高数据库在高并发下的吞吐量。

二、知识储备

MySQL的隔离级别:

Read Uncommitted(读未提交)
一个事务可以看到其他未提交事务的执行结果。
会导致脏读(Dirty Read)、不可重复读(Nonrepeatable Read)、幻读(Phantom Read)问题。
本隔离级别很少用于实际应用,因为脏读在大多数业务场景下是不允许的。

Read Committed(读已提交)
一个事务只能看见已经提交事务所做的改变。
这种隔离级别会出现不可重复读和幻读。因为某一事务期间,其他事务会有新的commit,所以同一select可能返回不同结果。

Repeatable Read(可重复读)
确保多个事务在并发读取数据时,会看到同样的数据内容。
这种隔离级别会出现幻读 。
这是MySQL的默认事务隔离级别。

Serializable(串行化)
最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个数据行上加上读写锁。大量的超时现象和锁竞争,因此性能较差。

会出现的几种问题:

更新丢失(Lost Update):当多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题 —— 最后的更新覆盖了其他事务所做的更新。解决方法是一个事务对数据进行更改但还未提交时,其他事务不能访问修改同一个数据。

脏读(Dirty Read):一个事务正在对一条记录做修改,在这个事务未提交前,这条记录的数据就处于不一致状态,若此时另一个事务也来读取同一条记录,读取到了这些尚未提交的脏数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被叫做 “脏读”。

不可重复读(Non-Repeatable Read):一个事务按时序先后多次读取某些数据,在此期间别的事务做了commit,数据已经发生了改变、或某些记录已经被删除了,导致同一事务中多次读取的数据不一样,这种现象叫做“不可重复读”。

幻读(Phantom Read):一个事务按时序多次检索数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为 “幻读”。

!!! ATTENTION !!!
不可重复读和幻读的区别在于,前者是数据内容改变,后者是条目数改变。记录删除在MySQL中是软删除,每条记录的头信息中有一个隐藏的特定位标记bit(deleted_flag)表示是否被删除1。可以使用optimize或alter指令来真正清理磁盘空间2

三、MVCC的引入

本文聚焦于 MySQL 中的 MVCC 实现,从 《高性能 MySQL》一书中对 MVCC 的介绍可知:

  1. MySQL 中 InnoDB 引擎支持 MVCC;
  2. 应对高并发事务, MVCC 比单纯的加行锁更有效, 开销更小;
  3. MVCC 只在读已提交(Read Committed)和可重复读(Repeatable Read)隔离级别下起作用,其他两个隔离级别和MVCC不兼容;(对于 Serializable 隔离级别,是通过加锁互斥来访问数据,因此不需要 MVCC )
  4. MVCC 既可以基于乐观(Optimistic)锁又可以基于悲观(Pessimistic )锁来实现。

四、InnoDB MVCC实现

版本控制

每一行记录有三个隐藏键,分别为DATA_TRX_ID、DATA_ROLL_PTR、DB_ROW_ID,其中:
DATA_TRX_ID
记录最近更新这条行记录的事务 ID,大小为 6 个字节

DATA_ROLL_PTR
表示指向该行回滚段(rollback segment)的指针,大小为 7 个字节,InnoDB 便是通过这个指针找到之前版本的数据。该行记录上所有旧版本,在 undo 中都通过链表的形式组织。

DB_ROW_ID
行标识(隐藏单调自增 ID),大小为 6 字节,如果表没有主键,InnoDB 会自动生成一个隐藏主键,即此列。

举个🌰

假设当前有一条记录,主键ID为1,插入这条记录的事务ID为100,此时有一个事务Tr1(事务ID为105)想要update这条记录,那么事务Tr1的流程为:

  1. 对 DB_ROW_ID = 1 的这行记录加排他锁;
  2. 把此次修改的反向操作记录到 undo log 3中,DB_TRX_ID (100)和 DB_ROLL_PTR 都不动;
  3. 修改该行的值,此时产生一个新版本,更新 DATA_TRX_ID 为105,将 DATA_ROLL_PTR 指向刚刚拷贝到 undo log 链中的旧版本记录。如果对同一行记录执行多次 Update,Undo Log 会组成一个链表,遍历这个链表执行记录的指令就可以将数据回滚到不同的版本上;
  4. 记录 redo log,包括 undo log 中的修改。

Insert和Delete操作也是类似的步骤。

ReadView的引入

现在我们有了版本控制,那么如何确定在某个时刻下,每个事务应该看见哪个版本呢?InnoDB使用ReadView,即快照这个概念来实现。

RR 下的 ReadView 生成

在 Repeatable Read 隔离级别下,每个事务执行第一个读请求时生成一份ReadView,后续所有的读请求都是复用这个 ReadView,由此解决了快照读模式下的脏读问题。需要注意的是,修改请求(update, delete, insert) 和ReadView没有关系,因为修改请求使用的是当前读而非快照读模式。

🌰
事务A在两次Select请求之间,事务B修改数据commit了,那么事务A的第二次Select依然读不到事务B修改后的数据。

RC下的ReadView生成

和RR的区别是,Read Commited隔离级别下,每一次的读请求都会生成一份ReadView,由于ReadView的更新,有可能造成前后读到的数据不一致,引入了脏读和幻读的问题。

🌰
在上面的例子中,事务A的第二次Select可以读取到事务B修改后的数据。

ReadView详解

ReadView中保存的 trx_sys 状态主要包括:

rw_trx_ids:当前活跃的事务ID数组,即ReadView初始化时当前未提交的事务列表;
low_limit_id:当前行最大事务ID。事务ID大于等于此值的事务对于view都是不可见的;
up_limit_id:rw_trx_ids 最小值,当前行最小活跃事务ID。事务ID小于此值的事务对于view一定是可见的;
low_limit_no:trx_no小于此值的undo log对于view是可以purge的。

那么我们可以根据当前事务ID判定可读数据版本了,假定当前行的DATA_TRX_ID为trx_id,判定逻辑如下:

  1. trx_id < up_limit_id:说明生成该版本的事务在 ReadView 生成前就已经提交了,所以该版本可以被当前事务访问。
  2. trx_id > low_limit_id:说明生成该版本的事务在生成 ReadView 后才生成,所以该版本不可以被当前事务访问。需要根据 Undo Log 链找到前一个版本,然后根据该版本的 DB_TRX_ID 重新判断可见性。
  3. trx_id 属于[up_limit_id, low_limit_id]:判断 trx_id 是否在 m_ids 列表中。如果在,说明创建 ReadView 时生成该版本所属事务还是活跃的,因此该版本不可以被访问,需要查找 Undo Log 链得到上一个版本,然后根据该版本的 DB_TRX_ID 重新判断可见性;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
    此时已经得到了这条记录相对 ReadView 的可见结果。如果这条记录的 delete_flag 为 true,说明这条记录已被删除,返回空,否则返回值即可。

关于当前读

除了ReadView实现的快照读,InnoDB对修改操作使用的是当前读模式,即读取的是数据的最新版本,并且当前读的记录都会加上锁,保证其他事务不会再并发的修改这条记录。具体的语句包括:

  1. select...lock in share mode (共享读锁)
  2. select...for update
  3. update , delete , insert

当前读的实现方式

当前读使用next-key锁(行记录锁+Gap间隙锁)实现。
间隙锁:只有在Read Repeatable、Serializable隔离级别才有,锁定了范围空间的数据。假设id有3,4,5,锁定id>3的数据,是指的4,5及后面的数字都会被锁定,此时如果想要加入新的数据id=6,由于间隙锁的存在无法Insert成功,避免了幻读。

间隙锁规则:

  1. 对主键或唯一索引,如果当前读时,where条件全部精确命中(=或者in),这种场景本身就不会出现幻读,所以只会加行记录锁。
  2. 没有索引的列,当前读操作时,会加全表gap锁,生产环境要注意。
  3. 非唯一索引列,如果where条件部分命中(>、<、like等)或者全未命中,则会加附近Gap间隙锁。

引用

  1. MySQL表剖析
  2. MySQL软删除
  3. MySQL Redo&Undo Log详解
  4. ReadView、回滚段、Purge详解
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269