MySQL高级系列--MVCC

其他网址

正确的理解MySQL的MVCC及实现原理-12172612-51CTO博客
数据库MVCC 隔离级别_数据库_Jaylon Wang的专栏-CSDN博客

简介

        多版本并发控制(Multi-Version Concurrency Control, MVCC),顾名思义,在并发访问的时候,数据存在版本的概念,可以有效地提升数据库并发能力,常见的数据库如MySQL、MS SQL Server、IBM DB2、Hbase、MongoDB等等都在使用。简单讲,如果没有MVCC,当想要读取的数据被其他事务用排它锁锁住时,只能互斥等待;而这时MVCC可以通过提供历史版本从而实现读取被锁的数据(的历史版本),避免了互斥等待。

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

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

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

  • 实时保留数据的一个或多个历史版本
  • 在需要时通过undo日志构造出历史版本

快照读与当前读

其他网址

【MySQL】当前读、快照读、MVCC - wwcom123 - 博客园

当前读

场景

select...lock in share mode (共享读锁)
select...for update
update , delete , insert

简介

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

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

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

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

  1.对主键或唯一索引,如果当前读时,where条件全部精确命中(=或者in),这种场景本身就不会出现幻读,所以只会加行记录锁。
  2.没有索引的列,当前读操作时,会加全表gap锁,生产环境要注意。
  3.非唯一索引列,如果where条件部分命中(>、<、like等)或者全未命中,则会加附近Gap间隙锁。例如,某表数据如下,非唯一索引2,6,9,9,11,15。如下语句要操作非唯一索引列9的数据,gap锁将会锁定的列是(6,11],该区间内无法插入数据。

快照读

场景

单纯select操作,不包括上述 select ... lock in share mode, select ... for update。    

  Read Committed隔离级别:每次select都生成一个快照读。
  Read Repeatable隔离级别:开启事务后第一个select语句才是快照读的地方,而不是一开启事务就快照读。

实现方式(undolog和多版本并发控制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

  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中MVCC是在Innodb存储引擎中得到支持的,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和回滚段是同一意思。
  • 锁(前边已有讲述)
  • 隔离级别(前边已有讲述)

MVCC实例

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

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

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

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

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

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

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

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

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

猜你喜欢

转载自blog.csdn.net/feiying0canglang/article/details/112917493