0
点赞
收藏
分享

微信扫一扫

MySQL原理--隔离级别的实现方式--MVCC


简介

        本文介绍MySQL的隔离级别的实现方式--MVCC。

        这也是Java后端面试题中常见的一个问题。

MVCC简介

        MVCC(Multi-Version Concurrency Control),含义为:多版本并发控制。

        在并发访问的时候,数据存在版本的概念,可以有效地提升数据库并发能力,常见的数据库如MySQL、MS SQL Server、IBM DB2、Hbase、MongoDB等等都在使用。

        简单讲,如果没有MVCC,当想要读取的数据被其他事务用排它锁锁住时,只能互斥等待;而MVCC可以通过提供历史版本从而能够读取被锁的数据(的历史版本),避免了互斥等待。

在 MySQL中,MVCC是 InnoDB 存储引擎实现隔离级别的一种具体方式。


  • 未提交读:无需使用 MVCC(总是读取最新的数据行)
  • 提交读可重复读:使用MVCC来实现。
  • 可串行化:需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

MVCC一般有两种实现方式(本文所讲的InnoDB采用的是后者)


  1. 实时保留数据的一个或多个历史版本
  2. 在需要时通过undo log构造出历史版本

快照读与当前读

当前读

什么时候是当前读?


  1. SELECT ... LOCK IN SHARE MODE (共享读锁)
  2. SELECT ... FOR UPDATE
  3. INSERT,UPDATE,DELETE 

简介

        当前读读取的是最新版本, 并且对读取的记录加锁,阻塞其他事务同时改动相同记录,避免出现安全问题

        例如,假设要update一条记录,但是另一个事务已经delete这条数据并且commit了,如果不加锁就会产生冲突。所以update的时候肯定要是当前读,得到最新的信息并且锁定相应的记录。

实现方式(next-key锁 ( 行记录锁+Gap间隙锁 ))

间隙锁:只在Read Repeatable、Serializable隔离级别才有,锁定范围空间的数据。假设id有3,4,5,锁定id>3的数据,是指的4,5及后面的数字都会被锁定,因为此时若不锁定没有的数据,例如当加入了新的数据id=6,就会出现幻读,间隙锁避免了幻读。


  1. 没有索引的列
  1. 当前读操作时,会加全表gap锁,生产环境要注意。
  1. 主键或唯一索引
  1. 如果当前读时,where条件全部精确命中(=或者in),这种场景本身就不会出现幻读,所以只会加行记录锁。
  1. 非唯一索引列

  1. 如果where条件部分命中(>、<、like等)或者全未命中,则会加附近Gap间隙锁。
  2. 例如,某表数据如下,非唯一索引2, 6, 9, 9, 11, 15。如下语句要操作非唯一索引列9的数据,gap锁将会锁定的列是(6,11],该区间内无法插入数据。


MySQL原理--隔离级别的实现方式--MVCC_数据库

快照读

什么时候是快照读?

单纯SELECT 操作,不包括:


  1. SELECT ... LOCK IN SHARE MODE (共享读锁)
  2. SELECT ... FOR UPDATE

提交读和可重复读的快照读


  • 提交读
  • 每次SELECT都生成一个快照读。
  • 可重复读

  • 开启事务后,第一个SELECT语句会快照读(对整个库拍了个快照)(不是一开启事务就快照读)。
  • 与可重复读的含义对应:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。


实现方式(undo log和MVCC)

        下图右侧绿色的是数据:一行数据记录,主键ID是10,name='Jack',age=10,  被update更新set为name= 'Tom',age=23。

  事务会先使用“排他锁”锁定该行,将该行当前的值复制到undo log中,然后再真正地修改当前行的值,最后填写事务的DB_TRX_ID,使用回滚指针DB_ROLL_PTR指向undo log中修改前的行DB_ROW_ID

MySQL原理--隔离级别的实现方式--MVCC_数据_02

  DB_TRX_ID: 6字节 DB_TRX_ID 字段,表示最后更新的事务id(update,delete,insert)。此外,删除在内部被视为更新,其中行中的特殊位被设置为将其标记为已软删除。

  DB_ROLL_PTR: 7字节回滚指针,指向前一个版本的undolog记录,组成undo链表。如果更新了行,则撤消日志记录包含在更新行之前重建行内容所需的信息。

  DB_ROW_ID: 6字节的DB_ROW_ID字段,包含一个随着新行插入而单调递增的行ID, 当由innodb自动产生聚集索引时,聚集索引会包括这个行ID的值,否则这个行ID不会出现在任何索引中。如果表中没有主键或合适的唯一索引, 也就是无法生成聚簇索引的时候, InnoDB会帮我们自动生成聚集索引, 聚簇索引会使用DB_ROW_ID的值来作为主键; 如果表中有主键或者合适的唯一索引, 那么聚簇索引中也就不会包含 DB_ROW_ID了 。  

  其它:insert undo log只在事务回滚时需要, 事务提交就可以删掉了。update undo log包括update 和 delete , 回滚和快照读 都需要。

原理

        事务ID是在mysql开启事务时为其分配的递增序列号,由于是递增的,所以可以基于此判断事务先后关系。

        MVCC的多版本指的是针对数据库中的一行数据,都可能通过undolog中的数据算出多条行数据,每行数据版本不同(是为多版本),针对每次写操作,事务提交前,都会在undolog中记录相应的变动(是为回滚log),以及对应的事务ID,再结合数据表中的当前行数据,就可以回溯出一个行的的多个版本了。

        Innodb会为每行数据添加两个字段 up_txid、del_txid,分别是更新事务ID、删除事务ID,事务新增或者更新一个数据行后,会将该事务ID记录在该行数据的up_txid中,事务删除行数据后,会将该事务ID记录在del_txid中。

在read repeatable隔离级别下

        该隔离级别下的事务启动时,除了分配上面说的事务ID外,系统还会查出当前活跃的事务ID列表(也就是开启了但还未提交的事务),分配给该事务存储下来,有了这些信息,就可以实现快照读了,RR隔离级别下,其查询到的行数据需要满足:


  1. 行数据的up_txid<=当前事务ID,并且不在活跃事务ID列表中
  2. 行数据的del_txid为null,或者>当前事务ID,或者在活跃事务ID列表中

        简单理解下,只查询在当前事务开启之前就已经提交的数据,并且这行数据未被删除或者在当前事务开启后删除,相当于事务启动时,拍了个快照,事务执行期间,就通过这个快照读取数据,其他事务的变动不会再对当前事务产生影响,是为可重复读

        在读取时,会从最新的一条数据开始读起,如果满足条件就以其为准,如果不满足就找到更旧的一行数据继续判断。

read committed隔离级别下

        和RR隔离级别一样的是,RC隔离级别下的查询也是快照读,区别就是RC隔离级别下每次select时都会获取下当前活跃事务ID列表,然后从最新一行数据开始,判断是否满足如下条件,不满足则继续判断更旧的一行数据:


  1. 行数据的up_txid不在活跃事务ID列表中,表示已经提交
  2. 行数据的del_txid为null,或者在活跃事务ID列表中未提交

简单理解下,就是每次都读取当前已经提交的并且未被删除的最新数据,相当于每次查询都会拍个快照

当前读

        如果查询加了锁,就不在mvcc的控制范畴了,因为此时用的是当前读 。当前读的规则,就是要能读到所有已经提交的记录的最新值。当前读是由锁来保证的。Innodb中有行锁,上面举例的几条语句,都会锁住id=1的这行数据,这样其他事务如果要对id=1这行数据进行当前读,只能等行锁释放,等到啥时候?事务完成的时候会释放掉锁,既然事务都完成了,那其他事务自然能读取到已提交的最新值。

MVCC

原理简述

简介

        在MySQL中,Innodb存储引擎支持MVCC。InnoDb的最基本的行中包含一些额外的存储信息:DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BITInnodb为每行记录都实现了三个隐藏字段:


  • 6字节的事务ID(DB_TRX_ID)。
    (该行所的事务id,每处理一个事务,其值自动+1。可以基于此判断事务先后关系)
  • 7字节的回滚指针(DB_ROLL_PTR)。
    (指向当前记录项的rollback segment的undo log记录,找之前版本的数据就是通过这个指针)
  • 6字节的隐式主键(DB_ROW_ID)。
    Innodb自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值,否则聚集索引中不包括这个值,这个用于索引当中。
  • 删除标识位(DELETE BIT)。
    用于标识该记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在commit的时候

MVCC并发控制的执行过程

以update为例:begin=> 用排他锁锁定该行=> 记录redo log=> 记录undo log=> 修改当前行的值,写事务编号


  • SELECT
    Innodb检查每行数据,确保他们符合两个标准:
    1、InnoDB只查找版本早于当前事务版本的数据行(也就是数据行的版本必须小于等于事务的版本),这确保当前事务读取的行都是事务之前已经存在的,或者是由当前事务创建或修改的行。
    2、行的删除操作的版本一定是未定义的或者大于当前事务的版本号,确定了当前事务开始之前,行没有被删除。
    符合了以上两点则返回查询结果。
  • INSERT
    InnoDB为每个新增行记录当前系统版本号作为创建ID。“创建时间”=DB_ROW_ID,这时,“删除时间 ”是未定义的;
  • DELETE
    InnoDB为每个删除行的记录当前系统版本号作为行的删除ID。
  • UPDATE
    InnoDB复制了一行。这个新行的版本号使用了系统版本号。它也把系统版本号作为了删除行的版本。

为了支持事务,Innbodb引入了下面几个概念:


  • redo log
            redo log就是保存执行的SQL语句到一个指定的Log文件,当Mysql执行recovery时重新执行redo log记录的SQL操作即可。当客户端执行每条SQL(更新语句)时,redo log会被首先写入log buffer;当客户端执行COMMIT命令时,log buffer中的内容会被视情况刷新到磁盘。redo log在磁盘上作为一个独立的文件存在,即Innodb的log文件。
  • undo log
            与redo log相反,undo log是为回滚而用。具体内容就是copy事务前的数据库内容(行)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘。undo buffer与redo buffer一样,也是环形缓冲,但当缓冲满的时候,undo buffer中的内容会也会被刷新到磁盘;与redo log不同的是,磁盘上不存在单独的undo log文件,所有的undo log均存放在主ibd数据文件中(表空间),即使客户端设置了每表一个数据文件也是如此。
  • rollback segment
            回滚段这个概念来自Oracle的事物模型,在Innodb中,undo log被划分为多个段,具体某行的undo log就保存在某个段中,称为回滚段。可以认为undo log和回滚段是同一意思。
  • 锁(前边已有讲述)
  • 隔离级别(前边已有讲述)

实例

有事务插入persion表插入了一条新记录:name为Jerry, age为24岁。可认为:隐式ID是1,事务ID和回滚指针,我们假设为NULL

MySQL原理--隔离级别的实现方式--MVCC_mvc_03

事务1对该记录的name做出修改,改为Tom

当事务1更改该行的值时,会进行如下操作:


  • 用排他锁锁定该行
  • 把该行数据拷贝到undo log中,作为旧记录(即在undo log中有当前行的拷贝副本)
  • 拷贝完毕后,有如下操作:
    修改该行name为Tom;
    修改隐藏字段的事务ID为当前事务1的ID(我们默认从1开始,之后递增);
    回滚指针指向拷贝到undo log的副本记录(即表示我的上一个版本就是它)。
  • 事务提交后,释放锁

MySQL原理--隔离级别的实现方式--MVCC_mysql_04



事务2修改person表的同一个记录,将age修改为30岁

当事务2更改该行的值时,会进行如下操作:


  • 用排他锁锁定该行
  • 把该行数据拷贝到undo log中,作为旧记录。
    发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面
  • 拷贝完毕后,有如下操作:
    修改该行age为30岁;
    修改隐藏字段的事务ID为当前事务2的ID, 那就是2
    回滚指针指向刚刚拷贝到undo log的副本记录
  • 事务提交后,释放锁

MySQL原理--隔离级别的实现方式--MVCC_mongodb_05

        从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,即:事务链,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。 

        因此,如果undo log一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的时在Innodb中存在purge线程,它会查询那些比现在最老的活动事务还早的undo log,并删除它们,从而保证undo log文件不至于无限增长。

其他网址

​​正确的理解MySQL的MVCC及实现原理-12172612-51CTO博客​​

​​MySQL是如何实现可重复读的? - InfoQ 写作平台​​

​​事务的可重复读的能力是怎么实现的? - Java学习指南​​


举报

相关推荐

0 条评论