多版本并发控制: MVCC

为什么需要MVCC

实现隔离性最简单的方式是串行化,而实现串行化最简单的办法就是加锁,但是很多应用的一个特点都是读多写少的场景,而读取数据间互相排斥显得不是很必要。所以就使用了一种读写锁的方法,读锁和读锁之间不互斥,而写锁和写锁、读锁都互斥。这样就很大提升了系统的并发能力。

但是之后人们发现并发读还是不够,又提出了能不能让读写之间也不冲突的方法,就是读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务会看到自己特定版本的数据,这样就又进一步提升了读-写事务的并发能力,这就是 MVCC 的基本原理。

InnoDB 实现MVCC原理

对于使用InnoDB 存储引擎来说,聚簇索引记录中都包含下面2个必要的隐藏列:

  • trx_id
    一个事务每次对某条聚簇索引记录进行改动时,都会把事务的事务ID赋值给 trx_id 隐藏列。
  • roll_pointer
    每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo 日志中,这个隐藏列相当于一个指针,可以通过它找到该记录修改前的信息。

假设插入该记录的事务ID 为80,那么此刻该条记录如下:

image.png

假设之后,事务ID分别为100,200 的事务对这条记录进行 UPDATE 操作。操作流程如下:

时间 trx100 trx200
1 BEGIN
2 BEGIN
3 UPDATE hero SET name='关羽' WHERE number=1
4 UPDATE hero SET name='张飞' WHERE number=1
5 COMMIT
6 UPDATE hero SET name='赵云' WHERE number=1
7 UPDATE hero SET name='诸葛亮' WHERE number=1
8 COMMIT

(这里是否可以在两个事务中交叉更新同一条记录呢?不可以,因为这就是一个事务修改了另一个未提交事务修改过的数据,沦为脏写。InnoDB 使用锁来保证不会出现脏写现象。也就是第一个事务更新某条记录前,就会给这条记录加锁,另一个事务再次更新该记录时,就需要等待第一个事务提交。)

每对记录进行一次改动,都会记录一条 undo 日志。每条 undo 日志 也都有一个 roll_pointer 属性(inert 操作对应的undo 日志没有该属性,因为insert操作的记录没有更早的版本)。通过这些属性可以将这些 undo 日志串成一个链表:

image.png

这个链表被称为版本链,版本链的头节点就是当前记录的最新值,同时,每个版本中也还包括了生成该版本时对应的事务ID,之后会利用这个记录的版本链来控制并发事务访问相同记录时的行为。

ReadView
事务隔离级别 脏写 脏读 不可重复读 幻读
未提交读(READ UNCOMMITTED)
已提交读(READ COMMITTED)
可重复读(REPEATABLE READ)
串行化(SERIALIZABLE)

对于使用 READ UNCOMMITTED 隔离级别来说,由于可以读到未提交事务修改过的记录(脏读),所以直接读取记录的最新版就好了。

对于使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说,必须保证读到已经提交的事务修改过的记录,也就是说假设另一个事务已经修改了记录但还未提交,则不能直接读取最新版本的记录。所以对于 MVCC 而言,这里的核心问题是:需要判断版本链中哪个版本是当前事务可见的?

为了解决这问题,提出了 ReadView (一致性视图)的概念。
这个 ReadView 主要包括4个比较重要的内容:

ReadView.png
  • m_ids 在生成 ReadView 时,当前系统中活跃的读写事务的事务id列表
  • min_trx_id 在生成 ReadView 时,当前系统中活跃的读写事务中最小的事务ID
  • max_trx_id 在生成 ReadView 时,系统应该分配给下一个事务的事务id值
  • creator_trx_id 生成该 ReadView 的事务的事务id

有了这个,在访问某条记录时,只需要按照下面的步骤来判断记录的某个版本的可见性:

  1. 如果被访问的版本的 trx_id 属性值与 值相同,意味着当前的事务在访问它字节修改过的记录,所以该版本可以被当前事务访问。

  2. 如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成的 ReadView 前已经提交,所以该版本可以被当前事务访问。

  3. 如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成的 ReadView 后才开启,所以该版本不可以被当前事务访问。

  4. 如果被访问版本的 trx_id 属性值 在 ReadView 中的 min_trx_idmax_trx_id 值之间,则需要判断trx_id属性值是否在m_ids列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

如果某个版本的数据对当前事务不可见,那就顺着版本链找到下一个版本的数据,并继续执行上面的步骤来判断记录的可见性;依次类推,直到版本链中的最后一个版本,如果记录中最后一个版本也不可见,就意味着该条记录对当前事务完全不可见,查询结果就不包含该记录。

在 MySQL 中,READ COMMTTE 和 REPEATABLE READ 隔离级别之间一个非常大的区别就是它们生成的 Read View 时机不同。

假设现在表 hero 中只有一条事务id 为 80 的事务插入的记录。

image.png
READ COMMITTED -- 每次读取数据前都生成一个 ReadView

比如,现在系统中有两个事务id分别为100,200 的事务正在执行:

时间 trx100 trx200
1 BEGIN
2 BEGIN
3 UPDATE hero SET name='关羽' WHERE number=1
4 更新一些其他表的记录
5 UPDATE hero SET name='张飞' WHERE number=1

那么此时 number 为 1 的记录对应版本链如图:

版本链.png

假设现在使用一个 READ COMMITTED 隔离级别的事务开始执行(是新事务,不是事务id 为100,200的那两个事务)SELECT 语句,查找这个 number 为 1 的记录,那么这个执行过程如下:

  1. 先生成一个 READVIEW,READVIEW 的 m_ids 列表内容就是 [100, 200],min_trx_id 为 100, max_trx_id 为201,creator_trx_id 为 0。

  2. 然后从版本链中挑选可见的记录,最新版本的 name 列内容是 '张飞',该版本的 trx_id 值也为100,也在 m_ids 列表内,因此也不符合要求,继续跳到下一个版本。

  3. 下一个版本的 name 列的 内容是'关羽',该版本的 trx_id 值也为100,也在 m_ids 列表内,因此不符合要求;继续跳到下一个版本。

  4. 下一个版本的 ame 列的 内容是'刘备' ,该版本的 trx_id 值为80,小于READVIEW中的 min_trx_id值100,所以这个版本是符合要求的;最后返回给用户的就是这条记录

之后,我们把事务id 为100的事务进行提交(事务200的还是未提交):

时间 trx100 trx200
1 BEGIN
2 BEGIN
3 UPDATE hero SET name='关羽' WHERE number=1
4 更新一些其他表的记录
5 UPDATE hero SET name='张飞' WHERE number=1
6 commit
7 UPDATE hero SET name='赵云' WHERE number=1
8 UPDATE hero SET name='诸葛亮' WHERE number=1

此时,表 hero 中 number 为 1 的记录的版本链如图所示:

版本链.png

然后,再到刚才使用 READ COMMITTED 隔离级别的事务中执行 SELECT 语句,继续查找这个 number 为 1 的记录,那么这个 执行过程如下:

  1. 在执行 SELECT 语句时又会单独生成一个 READVIEW,READVIEW 的 m_ids 列表内容就是 [200],因为事务100 已经提交了,min_trx_id 为 200, max_trx_id 为201,creator_trx_id 为 0。

  2. 然后从版本链中挑选可见的记录,最新版本的 name 列内容是 '诸葛亮',该版本的 trx_id 值也为200,在 m_ids 列表内,因此也不符合要求,继续跳到下一个版本。

  3. 下一个版本的 name 列的 内容是'赵云',该版本的 trx_id 值也为200,也在 m_ids 列表内,因此不符合要求;继续跳到下一个版本。

  4. 下一个版本的 ame 列的 内容是'张飞' ,该版本的 trx_id 值为100,小于READVIEW中的 min_trx_id值200,所以这个版本是符合要求的;最后返回给用户的就是这条记录

依次类推,如果事务id 为 200 的记录也提交了,再次使用 READ COMMITTED 隔离级别的事务中查询表 hero 中 number 为 1 的记录时,得到的结果就是 '诸葛亮' 了。

总结出来:使用 READ COMMITTED 隔离级别的事务在每次查询开始时都会生成一个独立的 ReadView

REPEATABLE READ -- 在第一次读取数据时生成一个 ReadView

对于使用 REPEATABLE READ 隔离级别的事务来说,只会在第一次执行查询语句时生成一个,之后的查询就不会重复生成 了。

还是,老例子,比如系统中有两个事务id 分别为100,200的事务正在执行:

此时,表 hero 中 number 为 1 的记录的版本链表如图所示:

版本链.png

假设现在有个一个使用 REPEATABLE READ 隔离级别的新事物开始执行,那么这个执行过程如下:

  1. 先生成一个 READVIEW,READVIEW 的 m_ids 列表内容就是 [100, 200],min_trx_id 为 100, max_trx_id 为201,creator_trx_id 为 0。

  2. 然后从版本链中挑选可见的记录,最新版本的 name 列内容是 '张飞',该版本的 trx_id 值也为100,也在 m_ids 列表内,因此也不符合要求,继续跳到下一个版本。

  3. 下一个版本的 name 列的 内容是'关羽',该版本的 trx_id 值也为100,也在 m_ids 列表内,因此不符合要求;继续跳到下一个版本。

  4. 下一个版本的 name 列的 内容是'刘备' ,该版本的 trx_id 值为80,小于READVIEW中的 min_trx_id值100,所以这个版本是符合要求的;最后返回给用户的就是这条记录

之后,我们把事务id为100的 事务进行提交(事务200的还是未提交):

时间 trx100 trx200
1 BEGIN
2 BEGIN
3 UPDATE hero SET name='关羽' WHERE number=1
4 更新一些其他表的记录
5 UPDATE hero SET name='张飞' WHERE number=1
6 commit
7 UPDATE hero SET name='赵云' WHERE number=1
8 UPDATE hero SET name='诸葛亮' WHERE number=1

此时表中的 记录的版本链如图:

版本链.png

然后,再到刚才使用 REPEATABLE READ 隔离级别的事务中执行 SELECT 语句,继续查找这个 number 为 1 的记录,那么这个 执行过程如下:

  1. REPEATABLE READ 隔离级别复用之前的READVIEW ,READVIEW 的 m_ids 列表内容就是 [100, 200],min_trx_id 为 100, max_trx_id 为201,creator_trx_id 为 0。

  2. 然后从版本链中挑选可见的记录,最新版本的 name 列内容是 '诸葛亮',该版本的 trx_id 值也为200,也在 m_ids 列表内,因此也不符合要求,继续跳到下一个版本。

  3. 下一个版本的 name 列的 内容是'赵云',该版本的 trx_id 值也为200,也在 m_ids 列表内,因此不符合要求;继续跳到下一个版本。

  4. 下一个版本的 name 列的 内容是'张飞' ,该版本的 trx_id 值为100,也在 m_ids 列表内,因此不符合要求;继续跳到下一个版本。同理,下一个版本 name 列的 内容是'刘备',也不符合要求,继续跳转到下一个版本。

  5. 下一个版本的 name 列的 内容是'刘备' ,该版本的 trx_id 值为80,小于READVIEW中的 min_trx_id值100,所以这个版本是符合要求的;最后返回给用户的就是这条记录

上面看出,在 REPEATABLE READ 事务的两次查询结果是一样的,记录的name列值都是'刘备',这就是可重复读的含义。如果再把之前事务id为200的记录进行提交,然后使用刚才的隔离级别继续查找,那么得到的结果还是'刘备'。

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

推荐阅读更多精彩内容

  • 介绍多版本并发控制 多版本并发控制技术(Multiversion Concurrency Control,MVCC...
    真正的飞鱼阅读 225评论 0 0
  • 并发控制 实现事务隔离的机制,称之为并发控制 所谓并发控制,就是保证并发执行的事务在某一隔离级别上的正确执行的机制...
    祁小彬阅读 1,014评论 0 2
  • 本篇作为学习笔记,文章内容来自“极客时间”专栏《MySQL实战45讲》,如有侵权,请告知,必即时删除。 举一个例子...
    JBryan阅读 425评论 0 2
  • mvcc是基于快照读取的,提高数据库的读写性能,在读取数据的时候不需要加锁 与之对应的是(当前读)加锁读取 m...
    张帆demo阅读 200评论 0 0
  • 一、并发控制基本知识 ​ 数据库是共享资源,通常有许多个事务同时在运行,当多个事务并发地存取同一个数据库时就会...
    song_songlll阅读 694评论 0 0