全网最清晰讲解MySQL的MVCC机制

06-01 1131阅读

一、背景

        网上很多讲解MySQL的MVCC机制,要么讲得晦涩难懂,要么很简单的逻辑讲得很复杂。后来我发现本质上,很多人只是讲了MVCC实现机制,并没有讲为什么要使用MVCC。 为什么要引入MVCC这个点很重要,如果没有MVCC那么会怎么样呢?

        如果没理解Why, 那么讲的东西就是真的云里雾里的。所以我把对MVCC的理解,以最简单、最通俗易懂、将它的来龙去脉讲清楚,和大家一起探讨。

全网最清晰讲解MySQL的MVCC机制

二、为什么要引入MVCC?

        MVCC(Multi-Version Concurrency Control)即多版本并发控制,是 MySQL 中用于实现高并发性能的一种机制,在提升并发性能的同时还能保证数据的一致性。

        我们知道,如果是多个线程分别操作不一样的数据,那么就没有所谓的并发控制。 所谓的并发控制,我们讨论的是 【多个线程/进程,同时对同一份数据执行查询或者更新操作】。

        这个同时被多个线程访问/操作的资源,我们称之为【临界区资源】

       大家自然而然想到 "锁机制",通过锁机制来控制对临界区资源的访问,来保证数据的一致性、正确性。

1、悲观锁

        悲观锁, 说白了就是利用MySQL物理层次强行加锁的机制。每个线程你要访问临界区资源,那么你们就来抢锁,无论再高的并发,始终都会有先到的请求和后到的请求, 那么谁拿到了锁,谁就拥有修改/查看数据的权力,其它没抢到锁的线程,抱歉,你给我阻塞等着,等我释放锁之后,你们继续抢锁,依次进行,直到所有的并发线程访问完毕。

        悲观锁,又分为  1、【共享锁/读锁】  2、【排它锁/写锁/独占锁】

1、共享锁/读锁

        读锁,有这么一个规则, 第一个线程给数据行加上读锁,OK, 那么其他人只能加读锁来读取数据,此时其他线程可以正常读数据, 但是不能加排它锁。

2、排它锁/写锁/独占锁

        写锁,规则更为严格, 只要第一个线程加了独占锁, 其它线程无法加读锁 也 无法加排它锁,只能阻塞等待,直到第一个线程释放锁,才能进行下一步操作。

        从上面来看,我们发现:

        缺点:

        通过MySQL物理层强制加锁的方式,悲观锁的性能比较差,因为一旦别的线程要么加了读锁,你就只能再加读锁,不能加排它锁。  更差性能情况就是,别人加了排它锁,你什么锁都无法加,阻塞直到释放排它锁。

        优点:

        悲观锁虽然有性能上的缺点,但是优点就是数据的安全性高啊,既然你们并发访问/修改,极大程度影响我的数据安全性,很简单,强制加锁,按序访问,就没这个问题了。

2、乐观锁

        乐观锁的机制,不是使用MySQL物理层次提供的SQL语法来强制加锁,而是通过逻辑上来解决这个并发问题。  “读”大家都可以“读”呗,你读数据又不影响数据的本身, 美女看是可以的,但是你别动手动脚就行对吧。 并发其实最怕的就是数据修改的问题,也就是写的安全性

         那么我可以这么做, 针对每个数据行,我使用一个version字段来标记,每次修改数据给这个version做个编号,代表我数据当前的版本。

        读数据都可以读,不限制,也不需要加锁(提高了并发性,因为很多场景都是读多,写少)

        我只需要关心最后更新数据的时候,去更新我自己刚开始读取到的version版本号的数据行即可, 如果能更新成功,那么代表别人没有在我写数据之前,更改过这个原始数据。

        如果更新不成功,那么代表在我更新数据之前,有人已经更新过数据了,那么我就不更新了,返回结果给调用者,让调用者决定,自己重试或者做其它逻辑处理。

        例如存在一行数据如下:

idnameageversion
1001rocky20100

        存在一个version字段,当前version是100。

        假设存在存在A、B  两个线程需要对age进行修改, A要增加10, B要减10。

        A 、B 同时读取到了这一行数据,但是A先更新执行了这个SQL:

update users set age = 30, version = version + 1 where id=1001 and version=100

         A提交之后, 原本的数据行version变成了101

idnameageversion
1001rocky30101

        此时B也在这一刹那(比A晚那么一丢丢), 也提交了更新操作相同的SQL

update users set age = 30, version = version + 1 where id=1001 and version=100

        请问B执行这条SQL成功吗?  很不巧,明显得到的更新影响行数是0. 因为where version=100根本不存在这条数据,被A更新过了,找不到了,vesion已经是101了。

        所以这就是乐观锁的优点, 只有在最后的更新时刻,才去检查并发冲突,如果产生冲突了,那么最差的结果就是你没更新成功,需要调用者自己决定更新策略。

         多个线程更新的过程,我们发现没有使用悲观锁, 强制从物理层面做排它锁或者读锁的机制,一个锁都没加, 实现了这种"无锁"机制, 提高了并发性能,也能保证了数据的一个安全性。 

3、引入MVCC解决了"读-写"并发问题

        通过前面的介绍,我们知道多线程访问数据库无外乎是以下场景:

        "读"-"读"、"读"-"写"、"写-写"

         MVCC 主要是解决  "读" -"写" 时候的并发问题,   "写"-"写"底层还是使用悲观锁(行锁/表锁机制),  "读-读"本身就不需要控制并发,都是读数据,不会对数据产生破坏。

1、没有引入MVCC之前, "读"-"写"如何实现?

        没有引入MVCC之前, "读" - "写" 完全使用上述说的,  悲观锁的机制实现。 读就加读锁, 写就加写锁,  如果有99人(线程)要看一个商品的详情数据,然后1个人正好在修改数据,并且修改数据的请求在你们读取数据请求之前被执行了。

        那么恭喜, 这99个人你给我阻塞等着, 因为前面有1个人正在在后台改商品数据【排它锁】, 等他改完了【释放锁】,你们才能继续查看。  试想,如果这是一个电商网站, 管理员后台改数据, 然后一堆人在逛商品详情, 管理员没改好,你们的网页就给我卡着不动,等我改完了你们才能继续看。

        这个明显是非常糟糕的体验,整个系统并发性能太差了。

        按照我们的逻辑应该是,你改就改嘛,我看下怎么咯, 最多一开始我看的时候是100元, 过一会后我再次刷新,看到新价格变为了50元, 那也没关系啊,对于管理员而言,你又没损失什么,只是客户拿到的信息稍微有点延迟了,不是实时看到最新数据而已嘛, 这个完全可以接受。

        没有引入MVCC之前,  "读" 会阻塞"写", "写"会阻塞"读", 性能极差.  那有没有什么好的办法,能够 "读"和"写" 不阻塞, 提高性能呢? 只是我稍微牺牲一点数据的实时性, 有可能看到的数据是稍微比较滞后的非实时数据, 这个也能接受。

        相对高性能来说,这么做是值得的。 因为就像举例的电商网站,如果是这种机制,公司业务都倒闭了,你还谈什么数据一致性?  所以,稍微的数据滞后问题,完全可以接受。

        鱼与熊掌不可兼得,我们要对业务场景,针对某些方面做折中和取舍

2、MVCC就是一种乐观锁的实现

         悲观锁性能差, 那么我们可不可以稍微牺牲一点数据实时性,采用乐观锁的机制来提高"读"-"写"的并发性能呢?  解决 "读的时候不阻塞写, 写的时候不阻塞读"的问题。

        MVCC就是MySQL的解决方案。 MVCC就是一种乐观锁的实现机制。

        MySQL 的 InnoDB 存储引擎在 4.0 版本开始引入 MVCC(多版本并发控制)。在这之前,数据库处理并发事务时主要依赖传统的锁机制,这虽然能保证数据的一致性,但在高并发场景下,锁的竞争会导致性能下降,影响系统的整体吞吐量。

        MVCC机制就能通过不加锁的机制,来解决 "读的时候不阻塞写, 写的时候不阻塞读"的问题, 提高了MySQL的性能.

        MVCC主要是应用在  RC(读已提交)、RR(可重复读) 这2种事务隔离级别下。

三、事务隔离级别

1、读未提交(read uncommit)

        RU级别, 会造成事务A能够读到事务B未提交的数据,可能会产生脏读、不可重复读、幻读。  

        性能最好,但是数据安全性最差. 这个事务隔离级别几乎不会有人去用.

2、读已提交(read commit)

        RC级别,解决了脏读问题,但是会产生不可重复读。 同一个事务,第一次SQL语句查询到的是A结果, 事务还没提交,再次执行相同查询SQL, 发现数据又变了,造成不可重复读。

        性能比RU稍差,但是数据安全性稍高. 

        Oracle数据库的默认事务隔离级别是RC

3、可重复读(repeatable read)

        RR级别,解决了不可重复读问题,但是会产生幻读。 同一个事务,第一次select count统计出来的数据,事务还没提交,再次执行相同查询SQL, 发现数据变了,造成幻读。

        性能比RC稍差,但是数据安全性稍高。

        MySQL数据库的默认事务隔离级别是RR

4、串行序列化(Serializable)

        串行序列化级别,没有前面的脏读、不可重复读、幻读问题,因为底层加了悲观锁(排它锁),所有的并发请求,一个个排队执行,只有前面拿到锁的执行完毕后,释放锁,后面的线程才能拿到锁,执行请求。

        性能最差, 但是数据安全性最高。

        这个几乎也没人会使用,除非数据安全性问题要求极高, 并发性能极差,全程加锁控制数据安全性。 和RU一样,走极端路线.

四、MVCC的基本原理

1、数据行-数据结构

        MVCC的基本原理是如下:

        1、每个数据行,都有隐藏的2个核心字段, DB_TRX_ID(更新数据行commit的事务ID)、DB_ROLL_PTR(更新数据前,上个版本的数据快照指针)

        2、每次更新数据行之后,每次会累积记录上次版本,然后DB_ROLL_PTR指向上个版本的数据行, 形成一个数据行链, 历史版本会记录在undo log中

        如图所示:

全网最清晰讲解MySQL的MVCC机制

        1、第一次, 事务ID=9  插入了这行数据, id=100   name=rocky  age=20, 所以DB_TRX_ID=9,   DB_ROLL_PTR=NULL,  因为前一个版本没有,所以指针是NULL

        2、第二次更新数据行,形成当前数据行, 事务ID=10,  DB_ROLL_PTR 指向上一个版本, DB_TRX_ID=10,  id=100  name=rocky  age=30

 2、ReadView

1、"当前读"、"快照读"

        读取当前实际数据行的数据,   这种我们称为"当前读". 一般用于实际UPDATE、DELETE等写操作,写的时候要进行"当前读",确保数据的一致性. 

        如果读取undo log版本链的数据,这种我们称为"快照读".  SELECT  查操作允许有滞后性,可以查询"快照读",不阻塞,提高并发性能

        我们前面提到的那个电商场景,客户正在读取出来的数据也许是"快照读", 就是从历史版本读取的, 原价100元.  但是针对管理员更改的数据为"当前读", 改为了50元, 管理员看到了最新的数据。  只有下一次,客户刷新页面,才能看到最新的数据50元.

2、Readview定位本事务要查询的、合理的"快照读"对应版本数据

        当前数据行,以及undo log存在历史版本数据行, 那我的事务开启的时候,SELECT查询, 我应该读取哪个版本的数据? 

        依靠以下的ReadView数据结构和判断规则,就能得到当前事务查询的一个合理"快照读"对应版本数据。

        ReadView的数据结构如下:

  1. m_low_limit_id
    • 这是当前系统里尚未分配的下一个事务 ID,也被称作高水位。其含义是,事务 ID 大于等于 m_low_limit_id 的记录,对于当前 ReadView而言是不可见的。
    • m_up_limit_id
      • 即低水位,它代表创建 ReadView 时当前系统里活跃事务中最小的事务 ID。事务 ID 小于 m_up_limit_id 的记录,对于当前 ReadView 是可见的。
      • m_creator_trx_id
        • 记录创建这个 ReadView 的事务 ID。若当前记录的 DB_TRX_ID 与 m_creator_trx_id 相等,那么该记录对当前事务可见。
        • m_ids
          • 这是一个存储创建 ReadView 时活跃事务 ID 的列表。活跃事务指的是那些已经开启但尚未提交的事务。当要判断某个记录是否可见时,需要查看该记录的 DB_TRX_ID 是否存在于 m_ids 列表中。        

五、ReadView的判断规则与事务隔离级别对应原理

1、ReadView的判断规则

        我总结这个规则其实很容易记,网上各种讲我感觉好乱、也不好记。

         首先,我们要知道事务ID是递增的,例如第一个事务编号是10,那么后面的编号是11、12、等等依次类推,编号越小,说明事务比事务ID大的更早启动了。

        首先从当前记录行开始按照下面的规则查找, 如果当前行找不到不符合规则,那么去undo log历史版本找,一直重复这个过程,直到找出  合理的 数据版本:

        1、若记录的 DB_TRX_ID 小于 m_up_limit_id(最小值),表明该记录是在创建 ReadView 之前就已经提交的事务修改的,所以该记录对当前事务可见。

        2、若记录的 DB_TRX_ID 处于 m_up_limit_id 和 m_low_limit_id 之间,就需要检查 DB_TRX_ID 是否存在于 m_ids 列表中:

          2.1、若存在,说明该记录是由一个尚未提交的活跃事务修改的,所以该记录对当前事务不可见。

          2.1、若不存在,说明该记录的修改事务在创建 ReadView 时已经提交,所以该记录对当前事务可见。

        3、若记录的 DB_TRX_ID 大于等于 m_low_limit_id,意味着该记录是在创建 ReadView 之后才开始的事务修改的,因此该记录对当前事务不可见。

        示意图如下, 查找规律:

全网最清晰讲解MySQL的MVCC机制

        从左到右,依次进行判断,这样更容易记住规则。 

2、事务隔离级别对应原理

        那有同学就疑惑了? MVCC怎么实现的RC级别的不可重复读,  RR级别下的可重复读呢?

        本质就是:

        1、RC级别下,每次进行SELECT查询都会创建一个ReadView,然后走ReadView规则查找对应的、合理的"快照读"版本数据。 那么每次间隔的相同SELECT,也会创建独立的ReadView,所以可能在这个间隔时间里面有别的事务提交了修改,那么对应得到的"快照读"版本也不一样,所以造成了不可重读。

        2、RR级别下,创建了第一次的ReadView以后,只要相同的SELECT查询语句,就不会再创建新的ReadView,而是复用这个ReadView,所以RR级别下可以实现 可重复读。

        这就是事务隔离级别RC、RR级别下的实现原理。

六、总结

        本章分享主要是从MVCC的来龙去脉,搞清楚Why 引入MVCC? MVCC解决了什么问题,没有MVCC之前又怎么解决的,MVVC解决方案好在哪些地方,由此才引出了ReadView、快照读、当前读、ReadView规则等等概念和底层原理分析。

        如果你搞不清楚Why, 光看MVCC知识点你也是云里雾里的感觉。如果你明确了解了为什么引入MVCC以及底层原理,才能完全吃透这个技术点,本身逻辑不复杂,被网上的一些晦涩难懂的讲解,只讲结果不讲来龙去脉的方式搞得懵了。

        如果本篇文章给你带来帮助,创作不易,帮忙一键三连加关注,如果存在瑕疵的地方,可以评论区探讨,谢谢~

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码