MySQL——MVCC--多版本并发控制机制

前言

以下的分析均在mysql的InnoDB引擎下。假设此时事务A与事务B同时执行。

一、定义:

MVCC(Multi-Version Concurrency Control,多版本并发控制)一种并发控制机制,在数据库中用来控制并发执行的事务,控制事务隔离进行。

二、核心思想:

MVCC是通过保存数据在某个时间点的快照来进行控制的。使用MVCC就是允许同一个数据记录拥有多个不同的版本。然后在查询时通过添加相对应的约束条件,就可以获取用户想要的对应版本的数据。

三、基本数据结构

1、redo log:

重做日志记录。存储事务操作的最新数据记录,方便日后使用。

2、undo log

撤回日志记录,也称版本链。当前事务未提交之前,undo log保存了当前事务的正在操作的数据记录的所有版本的信息,undo log中的数据可作为数据旧版本快照供其他并发事务进行快照读。每次有其它事务提交对当前数据行的修改,都是添加到undo log中。undo log是由每个数据行的多个不同的版本链接在一起构成的一个记录“链表”。如下图:

3、read_view(快照)

①read_view的简单理解:

会对数据在每个时刻的状态拍成照片记录下来。那么之后获取某时刻的数据时就还是原来的照片上的数据,是不会变的。其实也可以简单理解为是一个版本链的集合,只不过在这里的版本链是经过筛选的。

②read_view的基本结构:

read_view->creator_trx_id = 当前事务id; # 当前的事务id
read_view->up_limit_id = 12654;        # 当前活跃事务的最小id
read_view->low_limit_id = 12659;       # 当前活跃事务的最小id
read_view->trx_ids = [12654, 12659];   # 当前活跃的事务的id列表,又称活跃事务链表。表示在记录当前快照时的所有活跃的、未提交的事务
read_view->m_trx_ids = 2;              # 当前活跃的事务id列表长度

注意:

  • read_view中包含了活跃事务链表,这个链表表示此时还在活跃的事务,指的是那些在当前快照中还未提交的事务。(注意:新建事务(当前事务)与正在内存中commit 的事务不在活跃事务链表)。
  • read_view中不会显示所有的数据行,只会显示“可见”的记录。筛选方式如下所述。

③read_view的记录筛选方式:

前提:DATA_TRX_ID 表示每个数据行的最新的事务ID;up_limit_id表示当前快照中的最先开始的事务;low_limit_id表示当前快照中的最慢开始的事务,即最后一个事务。

  • 如果记录的DATA_TRX_ID < up_limit_id:在创建read_view时,修改该记录的事务已提交,该记录可被快照中的事务读取到(即可见)。

  • 如果DATA_TRX_ID >= low_limit_id:表示该记录是在当前read_view创建之后被其它事务修改的,该记录在当前快照中肯定不可见。此时需要从DB_ROLL_PTR指针所指向的回滚段中取出最新的undo-log的版本号, 然后用它继续重新开始整套比较算法。

  • 如果up_limit_id <= DATA_TRX_ID < low_limit_i:

    • 需要在活跃事务链表中查找是否存在ID为DATA_TRX_ID的值的事务。
    • 如果存在,那么因为在活跃事务链表中的事务是未提交的,所以该记录是不可见的。此时需要从DB_ROLL_PTR指针所指向的回滚段中取出最新的undo-log的版本号, 然后用它继续重新开始整套比较算法。(详细分析为什么“不可见”:因为DATA_TRX_ID只有在事务提交之后才会更新,而此时因为事务还存在于活跃事务链表中,所以说明事务是还没有commit,所以此时不可能存在对应的数据行,只有在当前事务提交之后才会有对应的数据行。)
    • 如果不存在,所以是可见的。(分析:按照上一点的对“不可见”原因的分析,可明白只能是当前本事务更新了这条记录,因为在当前read view中,只能是当前事务和正在内存中commit的事务不在事务活跃链表中,对于“正在内存中commit的事务”,因为它还没有commit,所以肯定是不可能读取到它的即将要commit的数据的,而所以只能是当前事务对这个数据行做了修改了,虽然未提交,但是因为是在当前事务中,所以肯定是可以读取到更新的数据的。

④read_view的更新方式:

注意:仅分析RC级别和RR级别,因为MVCC不适用于其它两个隔离级别。

  • a、对于Read Committed级别的:

    • 基本描述:每次执行select都会创建新的read_view,更新旧read_view,保证能读取到其他事务已经COMMIT的内容(读提交的语义);
    • 详细分析:假设当前有事务A和事务A+1并发进行。在当前级别下,事务A每次select的时候会创建新的read_view,此时可以简单理解为事务A会提交,也就是让事务A执行完毕,然后创建一个新的事务比如是事务A+2。这样子的话,因为事务A+2的事务ID肯定是比事务A+1的ID大,所以就能够读取到事务A+1的更新了。那么便可以读取到在创建这个新的read_view之前事务A+1所提交的所有信息。这是RC级别下能读取到其他事务已经COMMIT的内容的原因所在。
  • b、对于Repeatable Read级别的:

    • 第一次select时更新这个read_view,以后不会再更新,后续所有的select都是复用这个read_view。所以能保证每次读取的一致性,即都是读取第一次读取到的内容(可重复读的语义)。

注意:通过对read view的更新方式的分析可以得出:对于InnoDB下的MVCC来说,RR虽然比RC隔离级别高,但是开销反而相对少(因为不用频繁更新read_view)。

read_view的详细分析:https://www.iteye.com/blog/mahl1990-2347029

四、MVCC在mysql的具体实现:

4.1、基本数据结构的定义:

在mysql中,在实现MVCC时,会为每一个表添加如下几个隐藏的字段:

  • 6字节的DATA_TRX_ID:
    标记了最新更新这条行记录的transaction id,每处理一个事务,其值自动设置为当前事务ID(DATA_TRX_ID只有在事务提交之后才会更新);

  • 7字节的DATA_ROLL_PTR:
    一个rollback指针,指向当前这一行数据的上一个版本,找之前版本的数据就是通过这个指针,通过这个指针将数据的多个版本连接在一起构成一个undo log版本链;

  • 6字节的DB_ROW_ID:
    隐含的自增ID,如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引。这是一个用来唯一标识每一行的字段;

  • DELETE BIT位:
    用于标识当前记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在commit的时候。

MVCC在二级索引结构下的分析:https://www.cnblogs.com/stevenczp/p/8018986.html

4.2、增删改查:

①增加:INSERT
  • 设置新记录的DATA_TRX_ID为当前事务ID,其他的采用默认的。
②删除:DELETE
  • 修改DATA_TRX_ID的值为当前的执行删除操作的事务的ID,然后设置DELETE BIT为True,表示被删除。
③修改:UPDATE <==> INSERT + DELETE
  • 用X锁锁定该行(因为是写操作);
  • 记录redo log:将更新之后的数据记录到redo log中,以便日后使用;
  • 记录undo log:将更新之后的数据记录到undo log中,设置当前数据行的DATA_TRX_ID为当前事务ID,回滚指针DATA_ROLL_PTR指向undo log中的当前数据行更新之前的数据行,同时设置更新之前的数据行的DATA_TRX_ID为当前事务ID,并且设置DELETE BIT为True,表示被删除。
④查找:SELECT
  • 如果当前数据行的DELETE BIT为False,只查找版本早于当前事务版本的数据行(也就是数据行的DATA_TRX_ID必须小于等于当前事务的ID),这确保当前事务读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行;
  • 如果当前数据行的DELETE BIT为True,表示被删除,那么只能返回DATA_TRX_ID的值大于当前事务的行。获取在当前事务开始之前,还没有被删除的行。

注意:

  • a、此时就是要去查找read_view,判断其中是否有需要的记录;
  • b、就算在当前事务提交的时候,也不会读取到DATA_TRX_ID大于当前事务ID的数据记录(而默认情况下,RR隔离级别下,当前事务一commit,就能够读取到其他事务的commit)。这也是MVCC能够解决幻读的原因。

五、使用MVCC核心优势:

  • 1、在mysql中,使用MVCC本质上是为了在进行读操作的时候代替加锁,减少加锁带来的负担。
  • 2、在mysql的InnoDB引擎,并且是在RR隔离级别下,通过使用MVCC和gap锁来解决幻读问题。

六、MVCC与四大隔离级别的关系的分析:

分析了在MVCC的控制之下,如何实现四大隔离级别。

6.1、Read Uncimmitted级别:

由于存在脏读,即能读到未提交事务的数据行,所以不适用MVCC。原因是MVCC的DATA_TRX_ID只有在事务提交之后才会更新,而在Read uncimmitted级别下,由于是读取未提交的,所以说MVCC在这个级别下是不适用的。

6.2、Read Committed级别:

查找操作:
分析:假设当前有事务A、事务A+1、数据B(DATA_TRX_ID为A-1)。

  • 事务A进行查找,此时找出事务ID小于它本身的,所以此时数据B可以被找到;
  • 如果在事务A还没有执行完毕的时候,事务A+1对数据B进行了更新操作,那么此时数据B的undo log则被更新为“数据B(DATA_TRX_ID为A+1)-> 数据B(DATA_TRX_ID为A-1)”;
  • 此时如果事务A再次进行查找操作,会更新read_view。更新旧的read_view,并且开启新的事务A+2。那么根据MVCC的规定,就能够找到数据B(DATA_TRX_ID为A+1),可以找到更新之后的。这样子的话就等价于能够读取到别的事务commit的最新的数据记录。这就符合RC级别的语义。

6.3、Repeatable Read级别:

查找操作:
分析:假设当前有:事务A、事务A+1,数据B(DATA_TRX_ID为A-1)。

  • 事务A进行查找,此时找出事务ID小于它本身的,所以此时数据B可以被找到;
  • 如果在事务A还没有执行完毕的时候,事务A+1对数据B进行了更新操作,那么此时数据B的undo log则被更新为“数据B(DATA_TRX_ID为A+1)-> 数据B(DATA_TRX_ID为A-1)”;
  • 此时如果事务A再次进行查找操作,那么根据MVCC的规定,还是只能找到数据B(DATA_TRX_ID为A-1)(因为B(DATA_TRX_ID为A+1)的事务ID比当前事务A的事务ID大,所以不会被找到),不会找到更新之后的。这样子的话就等价于只能够读取到事务A开始时读取到的数据记录。这就符合RR级别的语义。

6.4、Serialization级别:

串行化由于是会对所涉及到的表加锁,并非行锁,自然也就不存在行的版本控制问题

总结:通过上面的分析可得:MVCC只适用于MySQL隔离级别中的读已提交(Read committed)和可重复读(Repeatable Read)

七、MVCC、gap锁解决幻读问题的分析:

前提:InnoDB引擎、RR隔离级别(gap锁只存在于这个级别下)

7.1、首先了解数据记录的读取方式:快照读和当前读

①快照读:

读快照,可以读取数据的所有版本信息,包括旧版本的信息。其实就是读取MVCC中的read_view,同时结合MVCC进行相对应的控制;

select * from table where ?;
②当前读:

读当前,读取当前数据的最新版本。而且读取到这个数据之后会对这个数据加锁,防止别的事务更改。

(分析:在进行写操作的时候就需要进行“当前读”,读取数据记录的最新版本)

select * from table where ? lock in share mode;  # 读锁
select * from table where ? for update;          # 写锁
insert into table values (…); 
update table set ? where ?; 
delete from table where ?;

详见:https://www.jianshu.com/p/27352449bcc0

③RC和RR隔离级别下的快照读和当前读:
  • RC隔离级别下,快照读和当前读结果一样,都是读取已提交的最新;
  • RR隔离级别下,当前读结果是其他事务已经提交的最新结果,快照读是读当前事务之前读到的结果。RR下创建快照读的时机决定了读到的版本。

7.2、解决幻读问题:

  • ①对于快照读:通过MVCC来进行控制的,不用加锁。按照MVCC中规定的“语法”进行增删改查等操作,以避免幻读。(MVCC的具体内容参见上方第1点到第4点的分析)

  • ②对于当前读:通过next-key锁(行锁+gap锁)来解决问题的。(next-key锁的分析:mysql中的锁

7.3、特殊语句分析:

“MVCC不能根本上解决幻读的情况?”

分析:这句话的含义是指对于快照读,那么是可以通过MVCC来解决的;但是对于当前读,则必须通过next-key锁(行锁+gap锁)来解决。

参考:
https://www.cnblogs.com/axing-articles/p/11415763.html

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

推荐阅读更多精彩内容