MySQL(二十一)MVCC多版本并发控制

发布时间 2023-07-18 09:14:21作者: Tod4

MySQL(二十一)MVCC多版本并发控制


1 什么是MVCC

  • MVCC(Multiversion Concurrency Control)多版本并发控制。即通过数据行的多个版本管理来实现数据库的并发控制,使得在InnoDB事务隔离级别下执行一致性读操作有了保障。
  • 就是为了查询一些正在被其他事务更新的值的时候,能够查到它们被更新之前的值,这样做就能在查询的时候不必等待更新事务的提交
  • MySQl中只有InnoDB支持MVCC,其他存储引擎不支持

2 快照读和当前读

  • MVCC在InnoDB中的实现主要是为了提高数据库的并发性能,用更好的方式处理读写冲突,做到即使有读写冲突,也能不加锁实现非堵塞并发读,这个读指的是快照读而不是当前读
  • 当前读实质上是一种加锁的操作,是悲观锁的体现;而MVCC是采用乐观锁的一种方式
2.1 快照读
  • 快照读又称作一致性读,对于普通的不加锁的简单SELECT都属于快照读,即不加锁的非堵塞读
  • 快照读的实现基于MVCC,在很多情况下,避免了加锁操作,降低了开销
  • 由于是多版本数据,所以快照读独到的可能不是最新的数据而是之前更新的历史版本
  • 快照读的前提是隔离级别不是可串行化可串行化快照读会退化为当前读
2.2 当前读
  • 当前读要求读取的是最新版本的数据
  • 还要求保证其他并发事务不能修改当前事务,会对读取的记录加锁
  • 加锁的SELECT(共享或排它锁)或者对数据进行增删改操作(自动添加排它锁)都会进行当前读

3 回顾

3.1 再谈隔离级别
image-20230508204516647

​ 在MySQL中默认的隔离级别是可重复读,从SQL标准的定义看它能解决脏读、不可重复读问题,但是不能解决幻读问题,如果想要解决幻读问题,需要提高隔离级别标准,设置为可串行化但响应地并发程度也会降低。

​ MVCC可以不使用可串行化的锁机制,而是通过乐观锁(MVCC )+ Next-key lock 临键锁的方式来解决幻读问题,可以在大多数情况下替代掉行级锁,降低系统的开销。

image-20230509094937068
3.2 隐藏字段、Undo log版本链

​ undo log的版本链,对于使用InnoDB存储引擎的表来说,它的聚簇记录中包含两个必要的索引列:

  • trx_id:每次事务对聚簇记录进行修改的时候,就会将该事务的id复制给trx_id隐藏列
  • roll_pointer:每次对每条聚簇索引进行改动的时候,都会将旧的版本信息写入undo log中,通过回滚指针就能找到记录修改前的信息

​ 比如插入一条记录,记录的示意图如下:

image-20230509095703376

insert log只在事务中起到回滚的作用,当事务提交之后,该类型的undo log记录就无效了,它占用的undo log segment也会被系统回收(undo log占用的页面链表要么被重用,要么被释放)

​ 假设两个事务id分别为10、20的事务分别对这条记录进行Update操作

image-20230509100311039

在InnoDB中,会对增删改操作自动添加排它锁,因此两个事务不会出现脏写的情况,也就是不会出现两个事务交叉着对同一条记录进行修改,必须等待第一个事务提交才能进行第二个事务

​ 每次对记录进行改动,都会记录一条undo log,每个undo log都包含创建它的事务id,每条undo log都会有一个roll pointerINSERT操作不会有,因为插入没有更新的版本),这些undo log通过roll pointer连接起来,串成一个链表,这个链表就成为undo log 版本链

image-20230509100559256

4 MVCC的实现原理--ReadView

​ MVCC的实现依赖于:隐藏字段Undo logRead View

4.1 什么是ReadView
  • 在MVCC中,多个事务对同一行记录进行更新会产生多个历史快照,这些记录保存在Undo Log里

  • Read View就是事务在使用MVCC机制在进行快照读操作时产生的快照

  • 快照记录创建这个Read View的事务id、活跃的事务中最小的id、系统最大的事务id,并且InnoDB会为每个事务构建了一个数组,用来记录并维护系统当前活跃事务的ID(活跃指的是启动了还没有提交)

  • 等到访问某条记录的时候,就可以根据上面记录的内容判断记录版本对当前事务可不可见

    • 如果Read Viewcreator_id当前事务的id相同,则意味着当前事务在访问它修改过的id,所以该记录版本可以被事务访问
    • 如果当前访问版本记录的trx_id小于Read Viewup_limit_id,则意味着修改该数据版本的事务已经提交,所以该版本的记录可以被当前事务访问
    • 如果当前访问版本记录的trx_id大于等于Read Viewlow_limit_id,则意味着创建该数据版本的事务是在ReadView生成之后才出现的,因此当前事务不能访问
    • 如果当前访问版本记录的trx_idRead Viewup_limit_idlow_limit_id之间,则需要判断trx_id是否在Read Viewtrx_ids活跃事务列表中,如果在则说明事务还没有提交当前事务不能访问,否则可以访问
4.2 ReadView的组成
  1. creator_id:创建这个Read View的事务id

  2. trx_ids:表示创建这个Read View的时候正在活跃的事务id列表

  3. up_limit_id:活跃的事务中最小的id

  4. low_limit_id:表示生成low_limit_id时系统应该分配给下一个事务的id值,low_limit_id是系统最大的事务id(而不是活跃的最大事务id)

    low_limit_id并不是trx_ids的最大值而是系统能够分配的事务id最大值,事务id是递增分配的,并且只有事务在进行增删改操作的时候才会分配事务ID。比如现在有1 2 5三个事务,那么id为5的事务提交后,一个新事务在生成ReadView的时候,trx_ids就包括1 2,up_limit_id就是1,low_limit_id就是6

举例

image-20230509103313272

​ 如上,此时如果有事务创建Read View,则

  • trx_ids=[trx2, trx3, trx5, trx8]
  • up_limit_id=trx2
  • low_limit_id=trx8+1
4.3 MVCC的整体流程

​ 当查询一条技术的时候,系统

  1. 首先获取查询操作的事务的版本号
  2. 获取当前系统的ReadView
  3. 将查询到的数据与ReadView中的事务版本号进行比较
  4. 如果不符合ReadView的规则,则通过回滚指针形成的Undo Log版本链undo log中获取符合规则的历史快照
  5. 返回符合规则的数据
4.4 隔离级别设计思路
  • 读未提交:能够读取未提交的事务修改的数据,所以直接读取最新的记录就可以,不必使用MVCC
  • 读已提交:不能读取未提交的事务修改的数据,并且不能进行重复读取,所以查询的时候每次都获取一次MVCCReadView视图
  • 可重复读:不能读取未提交的事务修改的数据,并且能进行重复读取,所以只在第一次查询的时候获取一次ReadView,之后查询都只查看已经生成的ReadView副本
  • 可串行化:InnoDB规定使用加锁的方式来访问记录
4.5 MVCC在可重复读下解决幻读问题

​ MySQL可以通过两种方式解决幻读问题

  • 读写加锁,也就是使用可串行化的隔离模式

  • 使用MVCC进行快照读,写使用临键锁

    添加的临键锁不会影响快照读,只会影响到想要获取锁的读操作

​ 可以回顾一下这一部分:MySQL在Repeatable Read隔离级别下是可以解决幻读问题的,解决的方案有两种:

  • 通过MVCC

    读操作利用多版本并发控制MVCC),写操作加

    MVCC就是生成一个ReadView,通过ReadView能够找到符合条件的记录版本(历史版本由undo log提供查询),查询语句执行查询已经提交的事务做出的更改,对于没由提交的事务和ReadView创建之后的事务做出的更改是看不到的。而写操作肯定是针对的最新版本的记录,因此读记录的历史版本和写操作的最新记录版本并不会冲突,也就是采用MVCC时,读写操作并不会冲突

    普通的SELECT语句在READ COMMITTED 和 REPEATABLE READ隔离级别下的读操作就是利用MVCC进行的读

    • READ COMMITTED:由于不会读取没有提交的事务修改的数据版本,因此避免了脏读问题
    • REPEATABLE READ:由于不会读取Read View创建之后的事务更改的数据(一个事务只有在第一次执行SELECT语句才会生成一个Read View,之后的SELECT语句都在复用),因此避免了可重复读和幻读问题
  • 通过加锁的方式

    读、写操作都采用加锁的方式

    在一些业务场景中,不允许读取数据的历史版本,即每次都需要去读取磁盘中最新的数据,这样也就意味着读操作也需要和写操作一样排队执行。

    如此一来,脏读不可重复读问题都得到了解决,因为读操作和写操作的串行执行,不会出现一个事务读取另一个未提交事务的数据以及一个事务读取过程中另一个事务修改数据提交导致前一个事务前后读取数据不一致的情况(第二个事务根本无法开始)

    ? 但是,幻读问题有些尴尬,试想一个事务在进行读操作,因此给表中的一定范围内的数据加锁,但是另一个事务要写的这个幻影数据可不在这个范围里面,也就是两个读写操作并不会冲突,仍然会出现幻读问题,解决这一个问题的办法就是写操作使用临键锁

4.6 总结

​ 通过MVCC可以解决:

  1. 读写之间的堵塞问题,提高事务的并发读写能力

  2. 降低了死锁的概率,MVCC采用了乐观锁的方式,读取数据的时候不需要加锁,对于写操作,也只要锁定必要的行

  3. 解决快照读问题,当查询数据库某个时间节点的快照的时候,只能查看到在这个节点之前提交的事务的结果而看不到时间点之后事务提交的更新结果