Mvcc 学习笔记

Leaving things better than you found them

MVCC 笔记


MVCC为了解决什么问题?

多版本并发控制,针对在并发访问数据库时对于数据版本的控制以及隔离性问题,Mysql使用了MVCC的思路来进行版本控制

MVCC的MYSQL 实现浅析?

Mysql 的 MVCC实现大致是通过隐藏列中的DB_ROLL_PTR字段以及undo log的方式生成数据版本链,在创建事务时生成ReadView来进行版本比对,从而筛选出当前事务可见的数据行

事务并发执行会遇到的问题?

脏写(Dirty Write):一个事务修改了另一个事务未提交过的数据

脏读(Dirty Read):一个事务读取到了另一个事务未提交过的数据

不可重复读(Non-Repeatable Read):一个事务只能读取到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事物都能查到最新的值(在一个事务中的多次查询,可以查询到多个其他事务提交的最新值)

幻读(Phantom):一个事务根据某些条件查询出一些记录之后,另一个事务又向表中插入了符合这些条件的记录,原先的事务用相同的条件再次查询时,能把另外一个事务插入的数据也查询出来

按问题严重性排序:

脏写 > 脏读 > 不可重复读 > 幻读

标准的四种 SQL事务隔离级别(并非Mysql定义)

Read UnCommittd:未提交读

Read Committd:已提交读

Repeatable Read:可重复读

Serializable:可串行化

隔离级别 脏读 不可重复读 幻读
Read UnCommittd Possible Possible Possible
Read Committd Not Possible Possible Possible
Repeatable Read Not Possible Not Possible Possible
serializable Not Possible Not Possible Not Possible

也就是说:

Read UnCommittd 隔离级别下,可能发生脏读不可重复读幻读问题

Read Committd 隔离级别下,可能发生不可重复读幻读问题

Repeatable Read 隔离级别下,可能会发生幻读但是在Mysql中,Repeatable Read隔离级别可以处理幻读的问题

serializable 隔离级别下,各种问题都不会发生

至于脏写,应为脏写实在太严重了,所以无论哪个隔离级别都不允许脏写的情况发生。

什么是版本链 ? undo日志 ?

undo日志:用于记录事务中未提交的变更记录,主要用于保证事务的原子性,任何对数据的操作都会记录到undo日志中,直到提交事务或者rollback,才会进行清理。

说起版本链,我们得先有行格式的概念,我们大概看一下Compact格式下所看到的一行数据的格式:

行格式.png

在一个正常的行信息中,除了记录了用户的真实记录以外,innoDB还会为每条记录都添加2个隐藏列以及一个可选列

列名 是否必须 占用空间 描述
DB_TRX_ID 6字节 事务ID
DB_ROLL_PTR 7字节 回滚指针
DB_ROW_ID 6字节 行id,唯一标识一条记录

DB_ROW_ID 在没有自定义主键以及存在非Null的Unique键时才会添加该列

这里来简单阐述一下DB_TRX_ID以及DB_ROLL_PTR在版本链中的作用

DB_TRX_ID:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给该记录的DB_TRX_ID隐藏列,注意事务id是递增的。

DB_ROLL_PTR:每次对某条聚簇索引记录改动时,都会将旧的版本写入到 undo日志中,然后然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

注意insert是不会产生DB_ROLL_PRT的,因为insert时并没有更早的版本存在

了解到这里我们大概就能看到版本链的雏形了,也就是利用了DB_ROLL_PTR来链接上一个版本的数据;

我们以一个hero表为例:


hero table.png

假如我们有一个hero表其中number为1的记录name初始化为刘备,我们执行如下两个语句:

hero 事务.png

它的版本链大概就是下面这个样子:


version list1.png

每次对该记录更新后,都会将旧值放到 undo 日志中,随着更新次数的增多,所有版本都会被DB_ROLL_PRT属性链接成为一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值,另外,每个版本中还包含生成该版本时对应的事务id

ReadView 是什么?

ReadView 可以按字面意思理解为读视图,也就是在事务开始时生成的一个快照,ReadView的设计主要是为了解决 "判断版本链中哪个版本是当前事务可见" 的问题

SERIALIZABLE隔离级别采用加锁的方式来访问记录,而READ COMMITTEDREPEATABLE READ隔离级别在事务的不同阶段会创建ReadView

Read committed 隔离级别下,每次读取数据前都会生产一个ReadView

Repeatable Read 隔离级别下,在第一次读取数据时生产一个ReadView

关于两种隔离级别下产生ReadView时机不同带来的影响,后面描述

ReadView 主要组成结构:

m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表

min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中最小的值

max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的事务id

  max_trx_id并非是是m_ids中的最大值,事务id是递增分配的,比方说现在有id为1,2,3这三个事务,之后id为3的事务提交了,那么一个新的读事务在生产ReadView时,m_ids时就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4

creator_trx_id:表示生成该ReadView的事务的事务id

  只有在对表中的记录做改动时(执行Insert、update、delete)才会为事务分配事务id,否则在一个只读事务中,事务id都默认为0

当生成了这个ReadView,这样在访问某条记录时,只需按照下边的步骤判断记录的某个版本是否可见(可见性要求):

  • 如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问他自己修改过的记录,所以该版本可以被当前事务访问

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

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

  • 如果被访问版本的trx_id属性值在ReadViewmin_trx_idmax_trx_id之间,那就需要判断一下trx_id属性是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问,如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问

Read Committd 每次读取数据前都生成一个ReadView

假如现在系统中有两个事务在执行,事务id分别是100200

trx 2.png

版本链如下:


version list2.png

假设现在有一个使用Read Committd隔离级别的事务开始执行:

trx read committed.png

那么这个Select1的执行过程如下:

  1. 在执行SELECT 语句时会现生成一个ReadViewReadViewm_ids列表内容为[100,200],min_trx_id为100,max_trx_id为201,creator_trx_id为0

  2. 然后从版本链中挑选可见记录,从图中可以看出,最新版本的列name的内容是 '张飞',该版本的事务id为100,在m_ids内,所以不符合可见性要求,根据DB_ROLL_PTR(roll_pointer)找到下一个版本

  3. 下一个版本的列name的值为 '关羽',该数据的事务id也为100,在m_ids范围内,不符合可见性要求,继续跳到下一个版本

  4. 下一个版本的列name的值为 '刘备',该版本的事务id为80,小于ReadView中的min_trx_id值100,所以这个版本符合可见性要求,最终返回给用户的就是这条name列为 '刘备' 的数据

之后,我们把事务id100的事务提交一下:

commit transaction.png

然后再到事务id为200的事务中更新一下hero表中number为1的数据:


update trx 200.png

此刻表hero中number为1的记录的版本链如下:


version list3.png

然后我们再到刚才使用Read Committd隔离级别的事务中继续查找这个number为1的记录,如下:

SELECT 2.png

其中的Select2的执行过程如下:

  1. 在执行SELECT2 语句时又会单独生成一个ReadView,该ReadViewm_ids列表内容是[200](事务id为100的那个事务已经提交了,所以再次生成快照时就没有它了),min_trx_id为200,max_trx_id为201,creator_id为0

  2. 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列name值为 '诸葛亮',该版本的事务id为200,在m_ids列表内,所以不符合可见性要求,根据DB_ROLL_PTR(roll_pointer)跳到下一个版本。

  3. 下一个版本的列name的值为 '赵云',该版本的事务id为200,在m_ids列表内,所以也不符合要求,继续跳到下一个版本

  4. 下一个版本的列name的值为 '张飞',该版本的事务id为100,小于ReadViewmin_trx_id的值200,所以符合要求,最后返回给用户的版本就是这条列name为 '张飞' 的记录

可以看到在Read Committd的隔离级别下,出现了不可重复读的场景

Read Committd隔离级别下,事务在每次查询开始时都会创建一个独立的ReadView,关于Repeatable Read隔离级别下版本链以及执行过程大概类似这里就不阐述了(欢迎讨论),只是在Repeatable Read隔离级别下,在事务中多次读数据时,只会在第一次读取数据时创建ReadView,后面的查询都会复用第一次创建的ReadView,这就保证了前后两次查询到的结果一致,可以尝试使用Repeatable Read隔离级别的特性去看看上面的版本链,select2在Repeatable Read级别下应该返回什么?怎么去理解可重复度?


总结:

从上边的描述中我们可以看出来,所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用Read CommittdRepeatable Read这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。Read CommittdRepeatable Read这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,Read Committd在每一次进行普通SELECT操作前都会生成一个ReadView,而Repeatable Read只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。

疑问?

undo log理论上会在事务提交后进行删除,那么版本链如何形成呢?

实际 insert undo 在事务提交之后就可以被释放了,update undo由于还需要支持MVCC,不能立即删除掉,实际在行结构中除了隐藏列还有一个delete mark的标记位,1代表删除,0代表未删除,用来记录数据是否被删除,所以在上面的版本链判断数据时并非是简单的判断事务id,同时还会考虑这个delete_mark标记,同时在mysql中,作者为了减少因为移除数据后的磁盘重新排列的性能问题,还搞了一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录要插入到表中的话,可能会把这些被删除记录占用的存储空间给覆盖掉,当然也并非所有被标记了删除都数据都是覆盖处理,这里就涉及到mysql的后台的purge线程的作用了,后面再去了解

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

推荐阅读更多精彩内容

  • 事务的四大特性(简称ACID) 数据库如果支持事务的操作,那么就具备以下四个特性: 1、原子性(Atomicity...
    gregoriusxu阅读 306评论 0 0
  • 事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在 MySQL 中,事务支持是在引擎层实现的。MySQL...
    itczl阅读 991评论 0 0
  • 【2018-03-03】开学同听“第一课” 今天是我第一次和孩子做同学,同窗同听,亲子共修,感觉棒棒哒。回眸一整天...
    林文斌阅读 226评论 0 0
  • 百度“等价交换”:等价交换是不同效用的商品按照它们各自具有的价格相交换。是商品交换的一般原则,是不同使用价值...
    爱冰淇淋的兔子阅读 1,057评论 0 1
  • 文/ 麦醺 大学毕业后,我漂在西北的一个三线城市里。当时刚上班的我和同事合租在一个50平...
    麦小醺阅读 769评论 2 3