一文搞懂MySQL事务的隔离性如何实现|MVCC

## 前言

MySQL有ACID四大特性,本文着重讲解**MySQL不同事务之间的隔离性**的概念,以及MySQL如何实现隔离性。下面先罗列一下MySQL的四种事务隔离级别,以及不同隔离级别可能会存在的问题。**事务隔离级别越高,多个事务在并发访问数据库时互相产生数据干扰的可能性越低,但是并发访问的性能就越差**。(相当于牺牲了一定的性能去保证数据的安全性)

下面这张表,展示了MySQL的四大隔离级别和伴随着的一些问题,下面详细介绍。

![image.png](https://upload-images.jianshu.io/upload_images/27804186-75ddfe7d5319c2d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

## 事务隔离级别

读未提交:多个事务同时修改一条记录,A事务对其的改动在A事务还没提交时,在B事务中就可以看到A事务对其的改动。

读已提交:多个事务同时修改一条记录,A事务对其的改动在A事务提交之后,在B事务中可以看到A事务对其的改动。

可重复读:多个事务同时修改一条记录,这条记录在A事务执行期间是不变的(别的事务对这条记录的修改不被A事务感知)。

串行化:多个事务同时访问一条记录(CRUD),读加读锁,写加写锁,完全退化成了串行的访问,自然不会收到任何其他事务的干扰,性能最低。

## 不同级别伴随的问题

脏读:A事务在提交前对一个字段的改动会被B事务感知,那么事务之间就很容易产生干扰,假如A对一个字段改动之后被B感知,但是A又回滚了事务,则对该字段的改动依旧保留在B的查询结果中,那么这样的数据就是脏数据(处于处理中间过程的数据)。

不可重复读:A事务对于一条记录的读取结果,在B事务对其修改并提交之后,A再次读取同一条记录会得到不同的结果。

幻读:侧重于A事务的同一个范围查询命令,前后两次得到不同的记录数量,原因是B事务可能对其进行了插入。

### 小结一下

通过阅读上面给出的内容,可以得到结论:

1.  读未提交隔离级别并没有对行数据的可见性做任何限制,所有事务之间的改动都是互相可见的,所以存在很多问题,不推荐使用;

2.  串行化隔离级别因为通过锁机制对记录的访问进行限制,所以安全性最高,但并发访问退化成串行访问,性能较低;

**因此本文将侧重于探究MySQL如何实现`读已提交`和`可重复读`两种隔离级别(也就是你听闻的MVCC多版本并发控制的实现),通过后面的学习你将理解`读已提交`隔离级别如何`解决脏读`,`可重复读`隔离级别如何更进一步`解决不可重复读`。**

**接下来我将向你介绍`undo 版本链`机制以及`read view`快照读机制,这两个机制相互配合是实现MVCC的核心,而`读已提交`和`可重复读`隔离级别的实现都是建立在这两个核心机制之上。**

## undo 版本链

undo 版本链就是指undo log的存储在逻辑上的表现形式,它被用于事务当中的**回滚操作**以及**实现MVCC**,这里介绍一下undo log之所以能实现回滚记录的原理。

对于每一行记录,会有两个隐藏字段:`row_trx_id`和`roll_pointer`,`row_trx_id`表示更新(改动)本条记录的全局事务id **(每个事务创建都会分配id,全局递增,因此事务id区别对某条记录的修改是由哪个事务作出的)** ,`roll_pointer`是回滚指针,指向当前记录的前一个`undo log版本`,如果是第一个版本则`roll_pointer`指向nil,这样如果有多个事务对同一条记录进行了多次改动,则会在`undo log`中以链的形式存储改动过程。

假如有两个事务AB,数据表中有一行id为1的记录,其字段a初始值为0,事务A对id=1的行的a修改为1,事务B对id=1的行的a字段修改为2,则`undo log版本链`记录如下:

![image.png](https://upload-images.jianshu.io/upload_images/27804186-fba593d990592d6d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

在上图中,最下方的undo log中记录了当前行的最新版本,而该条记录之前的版本则以版本链的形式可追溯,这也是事务回滚所做的事。那undo log版本链和事务的隔离性有什么关系呢?**那就要引入另一个核心机制:read view。**

## read view

read view表示快照读,这个快照读会记录四个关键的属性:

1.  `create_trx_id`: 当前事务的id

2.  `m_idx`: 当前正在活跃的所有事务id(id数组),没有提交的事务的id

3.  `min_trx_id`: 当前系统中活跃的事务的id最小值

4.  `max_trx_id`: 当前系统中已经创建过的最新事务(id最大)的id+1的值

**当一个事务读取某条记录时会追溯undo log版本链,找到第一个可以访问的版本,而该记录的某一个版本是否能被这个事务读取到遵循如下规则:(这个规则永远成立,这个需要好好理解,对后面讲解可重复读和读已提交两个级别的实现密切相关)**

1.  如果当前记录行的row_trx_id小于min_trx_id,表示该版本的记录在当前事务开启之前创建,因此可以访问到

2.  如果当前记录行的row_trx_id大于等于max_trx_id,表示该版本的记录创建晚于当前活跃的事务,因此不能访问到

3.  如果当前记录行的row_trx_id大于等于min_trx_id且小于max_trx_id,则要分两种情况:

    *  当前记录行的row_trx_id在m_idx数组中,则当前事务无法访问到这个版本的记录 **(除非这个版本的row_trx_id等于当前事务本身的trx_id,本事务当然能访问自己修改的记录)** ,在m_idx数组中又不是当前事务自己创建的undo版本,表示是并发访问的其他事务对这条记录的修改的结果,则不能访问到。

    *  当前记录行的row_trx_id不在m_idx数组中,则表示这个版本是当前事务开启之前,其他事务已经提交了的undo版本,当前事务可访问到。

配合使用`read view`和`undo log版本链`就能实现**事务之间`并发访问`相同记录**时,可以根据事务id不同,获取同一行的不同undo log版本(多版本并发控制)。**下面通过模拟并发访问的两个事务操作**,介绍MVCC的实现(具体来说就是**可重复读**和**读已提交**两个隔离级别的实现)

### 可重复读

下面模拟两个并发访问同一条记录的事务AB的行为,假设这条记录初始时id=1,a=0,该记录两个隐藏字段row_trx_id = 100,roll_pointer = nil

**注意:在可重复读隔离级别下,当事务sql执行的时候,会生成一个read view快照,且在本事务周期内一直使用这个read view**,下面给出了并发访问同一条记录的两个事务AB的具体执行过程,并解释`可重复读`是如何实现的(解决了`脏读`和`不可重复读`)。

![image.png](https://upload-images.jianshu.io/upload_images/27804186-c96a43c954e495b7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

事务A的read view:

`create_trx_id` = 101| `m_idx` = [101, 102]|`min_trx_id` = 101|`max_trx_id` = 103

事务B的read view:

`create_trx_id` = 102| `m_idx` = [101, 102]|`min_trx_id` = 101|`max_trx_id` = 103

(ps. 这里因为AB事务是并发执行,因此两个事务创建的read view的max_trx_id = 103)

![image.png](https://upload-images.jianshu.io/upload_images/27804186-59e2d85a036520d0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

**这里要注意的是,每次对一条记录发生修改,就会记录一个undo log的版本**,则在A事务中第二次查询id=1的记录的a的值的时候,B事务对该记录的修改已经添加到版本链上了,此时这个`undo log`的`trx_id = 102`,在A事务的`read view`的`m_idx数组`中且不等于A事务的`trx_id = 101`,因此无法访问到,需要在向前回溯,这里找到`trx_id = 100`的记录版本(小于A事务`read view`的`min_trx_id`属性,因此可以访问到),故A事务第二次查询依旧得到a = 0,而不是B事务修改的a = 1。

你可能有疑问,在A事务第二次查询的时候,B事务已经完成提交了,那么A事务的read view的m_idx数组应该移除102才对啊,它存的不是当前活跃的事务的id吗?·

**注意:在可重复读隔离级别下,当事务sql执行的时候,会生成一个read view快照,且在本事务周期内一直使用这个read view**,虽然102确实应该从A事务的read view中移除,但是因为read view在可重复读隔离级别下只会在第一条SQL执行时创建一次,并始终保持不变直到事务结束。

**那么也就明白了,在可重复读隔离级别下,因为read view只在第一条SQL执行时创建,因此并发访问的其他事务提交前改动的脏数据、以及并发访问的其他事务提交的改动数据都对当前事务是透明的(尽管确实是记录在了undo log版本链中)** ,这就解决了脏读和不可重复读(即使其他事务提交的修改,对A事务来说前后查询结果相同)的问题!

### 读已提交

还是借助上面事务处理的例子,所有的事务处理流程不变,**只是将隔离级别调整为读已提交,读已提交依旧遵守read view和undo log版本链机制,它和可重复读级别的区别在于,每次执行sql,都会创建一个read view,获取最新的事务快照。** 而因为这个区别,读已提交产生了不可重复读的问题,下面来分析一下原因:

![image.png](https://upload-images.jianshu.io/upload_images/27804186-75a7882aa7ca8911.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

事务A第一次查询创建的read view:

`create_trx_id` = 101| `m_idx` = [101, 102]|`min_trx_id` = 101|`max_trx_id` = 103

事务B的read view:

`create_trx_id` = 102| `m_idx` = [101, 102]|`min_trx_id` = 101|`max_trx_id` = 103

事务A第二次查询创建的read view:

`create_trx_id` = 101| `m_idx` = [101]|`min_trx_id` = 101|`max_trx_id` = 103

(ps. 这里因为AB事务是并发执行,因此两个事务创建的read view的max_trx_id = 103)

![image.png](https://upload-images.jianshu.io/upload_images/27804186-9e5da7c8ae524e80.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

这里重点观察A事务的第二次查询,之前你可能就意识到了,在事务B完成提交后,当前系统中活跃的事务id应该移除102,但是因为**在可重复读隔离级别下,A事务的`read view`只会在第一个SQL执行时创建,而在读已提交隔离级别下,每次执行SQL都会创建最新的read view**,且此时 `m_idx`数组中移除了102,那么事务A在追溯undo log版本链的时候,最新版本记录的`trx_id = 102`,102不在A事务的m_idx数组中,且`101 = min_trx_id <= 102 < max_trx_id = 103`,因此可以访问到B事务的提交结果。

**那么对A事务来说,在事务过程中读取同一条记录第一次得到a=0,第二次得到a=1,所以出现了不可重复读的问题(这里B不提交的话A如果就进行了第二次查询,则102不会从A事务的read view移除,则A事务依旧访问不到B事务未提交的修改,因此脏读还是可以避免的!)**

## 结束语

在我的理解中,MVCC多版本并发控制的实现可以理解成读已提交、可重复读两种隔离级别的实现,通过控制read view的创建时机(其访问机制是不变的),配合undo log版本链可以实现事务之间对同一条记录的并发访问,并获得不同的结果。

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

推荐阅读更多精彩内容