MySQL--MVCC Bertha 。 2023-01-09 12:34 123阅读 0赞 # **其他网址** # > [正确的理解MySQL的MVCC及实现原理-12172612-51CTO博客][MySQL_MVCC_-12172612-51CTO] > [数据库MVCC 隔离级别\_数据库\_Jaylon Wang的专栏-CSDN博客][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 - 博客园][MySQL_MVCC - wwcom123 -] ## **当前读** ## **什么时候是当前读?** > select ... lock in share mode (共享读锁) > select ... for update > 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锁,生产环境要注意。 > 2. 主键或唯一索引 > > 1. 如果当前读时,where条件全部精确命中(=或者in),这种场景本身就不会出现幻读,所以只会加行记录锁。 > 3. 非唯一索引列 > > 1. 如果where条件部分命中(>、<、like等)或者全未命中,则会加附近Gap间隙锁。 > 2. 例如,某表数据如下,非唯一索引2, 6, 9, 9, 11, 15。如下语句要操作非唯一索引列9的数据,gap锁将会锁定的列是(6,11\],该区间内无法插入数据。 > > ![dcd18cc09433ccca2bed6978414a2b39.png][] ## 快照读 ## **其他网址** > [MySQL是如何实现可重复读的? - InfoQ 写作平台][MySQL_ - InfoQ] > [事务的可重复读的能力是怎么实现的? - Java学习指南][- Java] **什么时候是快照读?** > 单纯select操作,**不包括**上述 select ... lock in share mode, select ... for update。 > > * 提交读 > > * 每次select都生成一个快照读。 > * 可重复读 > > * 开启事务后,第一个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**。 > > ![5c4d255d4cc3e3dde1ddf8dc28ff7098.png][] > > ** 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和回滚段是同一意思。 > * 锁(前边已有讲述) > * 隔离级别(前边已有讲述) ## 实例 ## 有事务插入persion表插入了一条新记录:name为Jerry, age为24岁。可认为:隐式ID是1,事务ID和回滚指针,我们假设为NULL > ![20190313213836406.png][] 事务1对该记录的name做出修改,改为Tom > 当事务1更改该行的值时,会进行如下操作: > > * 用排他锁锁定该行 > * 把该行数据拷贝到undo log中,作为旧记录(即在undo log中有当前行的拷贝副本) > * 拷贝完毕后,有如下操作: > 修改该行name为Tom; > 修改隐藏字段的事务ID为当前事务1的ID(我们默认从1开始,之后递增); > 回滚指针指向拷贝到undo log的副本记录(即表示我的上一个版本就是它)。 > * 事务提交后,释放锁 > > ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NuYWlsTWFubg_size_16_color_FFFFFF_t_70][] 事务2修改person表的同一个记录,将age修改为30岁 > 当事务2更改该行的值时,会进行如下操作: > > * 用排他锁锁定该行 > * 把该行数据拷贝到undo log中,作为旧记录。 > 发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面 > * 拷贝完毕后,有如下操作: > 修改该行age为30岁; > 修改隐藏字段的事务ID为当前事务2的ID, 那就是2 > 回滚指针指向刚刚拷贝到undo log的副本记录 > * 事务提交后,释放锁 > > ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NuYWlsTWFubg_size_16_color_FFFFFF_t_70 1][] > 从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,即:事务链,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。 > > 因此,如果undo log一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的时在Innodb中存在purge线程,它会查询那些比现在最老的活动事务还早的undo log,并删除它们,从而保证undo log文件不至于无限增长。 [MySQL_MVCC_-12172612-51CTO]: https://blog.51cto.com/12182612/2486731?source=dra [MVCC _Jaylon Wang_-CSDN]: https://blog.csdn.net/kingmax54212008/article/details/104104566 [MySQL_MVCC - wwcom123 -]: https://www.cnblogs.com/wwcom123/p/10727194.html [dcd18cc09433ccca2bed6978414a2b39.png]: /images/20221119/9db575b141db4ac8b6ca4776b9739a92.png [MySQL_ - InfoQ]: https://xie.infoq.cn/article/ab5096790639e5bf898d17662 [- Java]: https://ldbmcs.gitbook.io/java/shu-ju-ku/mysql/shi-wu-de-ke-zhong-fu-du-de-neng-li-shi-zen-mo-shi-xian-de [5c4d255d4cc3e3dde1ddf8dc28ff7098.png]: https://img-blog.csdnimg.cn/img_convert/5c4d255d4cc3e3dde1ddf8dc28ff7098.png [20190313213836406.png]: https://img-blog.csdnimg.cn/20190313213836406.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NuYWlsTWFubg_size_16_color_FFFFFF_t_70]: https://img-blog.csdnimg.cn/20190313220441831.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NuYWlsTWFubg==,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NuYWlsTWFubg_size_16_color_FFFFFF_t_70 1]: https://img-blog.csdnimg.cn/20190313220528630.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NuYWlsTWFubg==,size_16,color_FFFFFF,t_70
相关 mysqlmvcc-csdn 既然MySQL中InnoDB使用MVCC,为什么REPEATABLE-READ不能消除幻读 第一个问题: 新版本的mysql通过mvcc解决了幻读的问题,所以你没有看到 小咪咪/ 2023年09月25日 10:14/ 0 赞/ 106 阅读
还没有评论,来说两句吧...