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