失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > MySQL原理--隔离级别的实现方式--MVCC

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

时间:2024-01-28 21:12:11

相关推荐

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

原文网址:MySQL原理--隔离级别的实现方式--MVCC_IT利刃出鞘的博客-CSDN博客

简介

本文介绍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采用的是后者)

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

快照读与当前读

快照读

什么时候是快照读?

单纯SELECT 操作,不包括:

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

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

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

实现方式(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 , 回滚和快照读 都需要。

当前读

什么时候是当前读?

SELECT ... LOCK IN SHARE MODE (共享读锁)SELECT ... FOR UPDATEINSERT,UPDATE,DELETE

简介

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

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

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

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

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

原理

说明

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

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

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

在read repeatable隔离级别下

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

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

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

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

read committed隔离级别下

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

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

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

当前读

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

MVCC

原理简述

简介

在MySQL中,Innodb存储引擎支持MVCC。

MVCC并发控制的执行过程

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

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

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的时候

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

redo log redo log用于故障恢复。MySQL会将执行的SQL语句保存到一个指定的Log文件,当MySQL执行recovery时重新执行redo log记录的SQL操作即可。当客户端执行每条SQL(更新语句)时,redo log会被首先写入redo log buffer;当客户端执行COMMIT命令时,log buffer中的内容会被视情况刷新到磁盘。redo log在磁盘上作为一个独立的文件存在,即Innodb的log文件。undo log undo log用于回滚。 MySQL会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。

事务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文件不至于无限增长。

其他网址

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

数据库MVCC 隔离级别_数据库_Jaylon Wang的专栏-CSDN博客

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

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

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

如果觉得《MySQL原理--隔离级别的实现方式--MVCC》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。