MySQL -- MVCC

作者: sunyelw | 来源:发表于2019-07-13 09:28 被阅读0次

    前言

    最近在学MySQL,决定记录一下,能写多少写多少,不定时更新,加油。

    正文

    分几个部分来吧,大致如下:

    • 字符集与比较规则

    • 行格式与数据页

    • InnoDB索引

    • 访问方法与连接

    • explain 与 子查询优化

    • redoundo 日志

    • MVCC 与 锁

    本文为第四部分 MVCC 原理解析


    主要就是想先写这个, 其他的后面有空再补~

    由于我们跳过一些东西, 下面说MVCC的原理可能会各种懵


    所以, 我们先简单过几个点吧.

    一、温故知新 -- 两个隐藏列的含义

    行格式的时候说过,InnoDB会为每行加上两个隐藏列(row_id并不是必加的)trx_idroll_pointer 这俩哥们简直生猛的一塌糊涂.

    小声明:
    事务T1的编号是100
    事务T2的编号是200

    • trx_id 这个明显是事务ID的意思, 就是记录一下最近操作此条记录的事务ID. 比如事务T1中插入了一条记录X,那X的trx_id就是100,如果在T1中继续操作这条记录是不会修改X的trx_id的.如果事务T2修改了这X记录,那么其trx_id就变成200.
    • roll_pointer字面意思就是回滚指针, 指针存放的是一个地址, 指向了某个值. 它指向的是一条undo日志记录.

    这里简单提一下undo日志,后面会专门写一篇详细讲的[有生之年系列]。

    一条undo日志就是一条记录, 存放在页中,叫undo日志页.大致分为两种,插入类型(insert)和修改类型(updatedelete),这里只需要知道他们有一个很大区别:插入类型的undo日志是没有指向下一条undo日志的属性的,也就是说他们组成了一个undo日志链表,又称版本链

    undo日志链表(版本链)
    • 第一条是真实记录,0是记录类型,H是记录头,300是trx_id R是roll_pointer, 1、3.. 是记录的真实数据
    • 可以看到每条undo日志都有一个事务ID(图中我画在最后面,实际并不是存在最后),这个属性其实是叫old trx_id, 表示当前undo日志对应的事务ID,也称此版本的创建事务ID
    • 插入类型undo日志没有old roll_pointer指向上一条roll_pointer,因为它本来就是版本链的第一条
    • 这条undo链表可以看出,此记录由事务ID为100的事务插入,在事务ID为200的事务中修改了两次,而事务ID为300的事务正在修改此记录。
    • 记住这个顺序,后面有用。

    二、老生常谈 -- 隔离级别

    说这个隔离级别之前,我们先想想为什么要有这个东东?

    其实每个新技术或新名词的出现, 都可以问这几个问题
    1.这个东东解决了什么问题吗?
    2.现有的技术解决不了吗?
    3.如果能, 它比当前的解决方案强在哪些方面呢?
    4.不足或改进之处.

    • 以上纯属扯淡

    随着互联网的发展,并发已经是一道绕不开的坎。各种问题都不断冒出来,那并发事务访问数据库会发生什么样的问题呢?

    一个一个来看下。

    • 脏写(Dirty Write
      T1事务开启
      T1修改:X=5
      T2事务开始
      T2修改:X=6
      T2事务提交
      T1事务提交

    结果:X=5

    这个时候T2的事务所做修改就丢失了.

    一个事务修改了另一个未提交事务修改过的数据,此为脏写。

    • 脏读(Dirty Read
      数据状态:X=5
      T1事务开启
      T2事务开始
      T2修改:X=6
      T1读取:X=6
      T2事务回滚
      T1事务提交

    T1读到的X=6,库中X=5

    一个事务读取了另一个未提交事务修改过的数据,此为脏读。

    • 不可重复读(Non-Repeatable Read
      事务T1开启
      事务T2开启
      T1读取:X=5
      T2修改:X=6
      事务T2提交
      T1读取:X=6
      。。。

    T1两次读取到的同一条记录的值不一样。

    每次事务提交后,当前事务都能读取到记录的最新值,此为不可重复读。

    • 幻读(Phantom
      库中数据: X=6
      事务T1开启
      事务T2开启
      T1读取:X>5 (得到一条X=6)
      T2新增:X=7
      T1读取:X>5 (得到两条X=6和X=7)
      。。。

    T1第二次读取的记录数量比第一次读取到的记录数量多。

    如果事务T1根据条件N查询数据,事务T2添加了满足条件N的记录并提交了,T1再次根据N查询数据能查询T2新增的记录,此为幻读。

    注意几个点:

    • 不可重复读是针对单条记录的改动(包括删除与修改)
    • 幻读是针对查询条件的范围内记录的新增
    • 幻读只是针对新增,如果有范围内记录的删除或修改,都属于不可重复读

    为了解决这些问题,有一帮人提出了一个SQL标准,给出了四种隔离级别,用以解决上诉问题:

    • READ UNCOMMITTED
    • READ COMMITTED
    • READ REPEATABLE
    • SERIALABLE

    SQL标准中规定,

    • READ UNCOMMITTED 解决脏写
    • READ COMMITTED 解决脏读
    • READ REPEATABLE 解决脏读与不可重复读
    • SERIALABLE 解决幻读

    数据库对脏写的问题是零容忍,哪怕最低的隔离级别都不允许出现

    然而MySQL里的大佬还是牛逼,他们在READ REPEATABLE 级别就已经解决了幻读问题。

    实现一般有两种方式,第一是加锁,第二是MVCC

    实际上,二者都有用到.


    三、千呼万唤 -- MVCC的原理

    ReadView登场

    先看下其大致结构

    ReadView结构
    • m_ids 存放当前系统中活跃的事务集合
    • min_trx_idm_ids中最小的事务ID
    • max_trx_id 分配给下一个开启事务的事务ID
    • creator_trx_id 创建此ReadView的事务ID

    事务ID注意两点:

    • 只有在对表中的记录做改动时(执行INSERTDELETEUPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
    • 按分配顺序递增

    怎么突然蹦出来一个ReadView? 先看看怎么用.

    如果你不是一条鱼的话,应该还记得前面说过,每条记录都有一个版本链吧,当一个事务要访问某条记录时,对着这个ReadView的操作是这样的:

    • 判断trx_idcreator_trx_id是否相等,是则意味着此版本正在被当前事务操作,可以访问
    • 判断trx_id是否小于min_trx_id,表示此版本的生成事务已经提交,可以访问
    • 判断trx_id是否大于等于max_trx_id,表示此版本的生成事务已经提交,可以访问
    • 判断trx_id是否在m_ids中,若不在,则意味着此版本的创建事务已经提交,可以访问;若在,则表明此版本在创建ReadView时还在活动,不能访问。

    若无法访问则顺着版本链找下一个版本,如果到最后一个版本(也就是Insert的undo日志)仍无法访问,那么此记录对当前事务不可见。

    现在知道这玩意有多牛逼了吧~

    那跟隔离级别有啥关系呢?

    READ COMMITTEDREAD REPEATABLE 的最大区别就是生成ReadView的时机不一样

    • READ REPEATABLE 事务开启时生成
    • READ COMMITTED 每次查询前生成

    想想ReadView与其生成时机如何能解决脏读/幻读问题~


    喊我来加班,到公司都写完一篇MVCC了,还没见到人~~~~[允悲]

    相关文章

      网友评论

        本文标题:MySQL -- MVCC

        本文链接:https://www.haomeiwen.com/subject/jutpkctx.html