实战MYDB的mvcc与mysql的mvcc详解

前言

最近做的一个简易版的MySQL-MYDB。里面也实现了mvcc。趁这个机会结合项目深入理解一下mvcc的机制。

先来看MySQL的mvcc讲了啥?

一 MVCC的作用


1.1 mvcc的作用

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

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

1.2 快照读 与 当前读 的区别与联系


1.MVCC在InnoDB中的实现主要是为了提高数据库的并发性能,用更好的方式处理读写冲突做到即使有读写冲突,也能不加锁实现非堵塞并发读,这个读指的是快照读而不是当前读。

2.当前读实质上是一种加锁的操作,是悲观锁的体现;而MVCC是采用乐观锁的一种方式 

1.3 快照读


1.快照读,顾名思义读取的是一份快照数据,所以读到的并不一定是最新数据,可能是历史数据。

2.简单的select查询就是快照读,不加锁非阻塞读,降低数据库的开销。

3.但是快照读在隔离级别是 “串行化级别” 是没有意义的,因为串行化的sql都是排队执行的,不存在并发,所以就会变成当前读。

1.4 当前读


当前读获取的数据是最新数据,而且在读取时不能被其他修改的,所以会对读取的记录加锁来控制。

加锁的SELECT(共享或排它锁)或者对数据进行增删改操作(自动添加排它锁)都会进行当前读。

select * from student where id > 1 lock in share mode;
// 或者
select * from student where id >1 for update;


1.5 mvcc可以解决问题


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

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

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

1.6 mvcc面试题:mvcc是怎么实现的


mvcc 是多版本并发控制,通过生成记录的历史版本解决幻读问题,并提高数据库的性能,无锁实现读写并发操作。

  • 1.mvcc 的实现主要是通过三个隐藏字段,undo log 以及readView 实现的。
  • 2.三个隐藏字段分别是隐藏主键,事务ID,回滚指针。
  • 3.undo log是各个事务修改同一条记录的时候生成的历史记录,方便回滚,同时会生成一条版本链。
  • 4.readView是事务在进行快照读的时候生成的记录快照,用于判断数据的可见性。
  • 5.描述readView 可见性判断规则。

二  MVCC实现原理


2.1 原理


​ MVCC的实现依赖于:隐藏字段、Undo log、Read View 

2.2 undo log


2.2.1 undo Log的作用


所谓的版本链就是在MVCC中,多个事务对同一行记录进行更新会产生多个历史快照,这些记录保存在Undo Log里,这些undo日志通过回滚指针串联在一起。

undo log就是回滚日志,在insert/update/delete变更操作的时候生成的记录方便回滚。 

当进行insert操作的时候,产生的undo log只有在事务回滚的时候需要,如果不回滚在事务提交之后就会被删除。

当进行update和delete的时候,产生的undo log不仅仅在事务回滚的时候需要,在快照读的时候也是需要的,所以不会立即删除,只有等不再用到这个日志的时候才会被mysql purge线程统一处理掉(delete操作也只是打一个删除标记,并不是真正的删除)。

2.2.2 undo Log的组成


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

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

2.2.3 undo Log的案例


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

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

3.如下图:

2.3 隐藏字段


除了我们正常业务涉及的字段外,InnoDB在内部向数据库表中添加三个隐藏字段:

  • 1.DB_TRX_ID:6-byte的事务ID。在插入或更新行时的最后一个事务的事务ID
  • 2.DB_ROLL_PTR:7-byte的回滚指针。就是 指向 对应的某行记录 的上一个版本,在undo log中使用。
  • 3.DB_ROW_ID:6-byte的隐藏主键。如果数据表中没有主键,那么InnoDB会自动生成单调递增的隐藏主键(表中有主键或者非NULL的UNIQUE键时都不会包含 DB_ROW_ID列)

如上面的表没有设计primary key,其中id/name/city是我们的业务字段,那么加上隐藏字段应该如下

2.4  ReadView


2.4.1 ReadView的作用


ReadView 是事务 快照读 的时候产生的 数据读视图,在该事务执行 快照读 的那一刻,会生成一个 数据系统 当前的快照,记录并维护系统当前活跃事务的id,事务的id值是递增的。

Read View就是事务在使用MVCC机制在进行快照读操作时产生的快照,ReadView 的最大作用就是判断数据的可见性当某个事务执行快照读的时候,会对此记录创建一个ReadView 的视图,在整个事务期间根据某些条件判断该事务能够看到的版本链上的哪条历史数据。

2.4.2  ReadView的组成

  • creator_id:创建这个Read View的事务id
  • trx_ids:表示创建这个Read View的时候正在活跃的事务id列表
  • up_limit_id:活跃的事务中最小的id
  • 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

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

trx_ids=[trx2, trx3, trx5, trx8]
up_limit_id=trx2
low_limit_id=trx8+1

2.4.3  ReadView的判断流程


当查询一条数据的时候,系统首先获取查询操作的事务的版本号 


获取当前系统的ReadView


将查询到的数据与ReadView中的事务版本号进行比较


如果不符合ReadView的规则,则通过回滚指针形成的Undo Log版本链,从undo log中获取符合规则的历史快照


返回符合规则的数据


快照记录创建这个Read View的事务id、活跃的事务中最小的id、系统最大的事务id,并且InnoDB会为每个事务构建了一个数组,用来记录并维护系统当前活跃事务的ID(活跃指的是启动了还没有提交),等到访问某条记录的时候,就可以根据上面记录的内容判断记录版本对当前事务可不可见:

  • 1.如果当前被访问记录的trx_id属性值与ReadView中的creator_trx_id值相同,说明当前事务修改的记录就是在当前事务下操作的,那当然是对我们可见的了,因此可以修改这条记录
  • 2.如果当前被访问记录的trx_id属性值小于ReadView中up_limit_id值,说明(生成该版本的事务在该事务生成readView之间已经提交)    即当前事务在开启的时候,这条记录最近依次被其他事务操作的事务已经提交了,所以对这条记录对我们来说也是可以见,可以修改
  • 3.如果当前被访问记录的trx_id属性值大于或者等于ReadView中low_limit_id值,说明(生成该版本的事务在当前事务生成readView之后才开启)     即:我们开启事务未修改该记录之前,已经有另外一个事务开启,并且正在修改该事务了,因此,这条记录对我们来说依然是不可见的,我们不能修改。
  • 4.如果当前被访问记录的trx_id属性值介于ReadView中 up_limit_id 和 low_limit_id 之间的话,那么此时就需要分情况讨论了,此时我们应该分析该trx_id是否在 trx_ids 中

如果存在,说明(创建ReadView时,生成该版本的事务还处于活跃状态)    即:当前已经有其它的事务正在修改该条记录,并且还未提交,此时这条记录对我们不可见

如果不存在,说明((创建ReadView时,生成该版本的事务已经提交)   即:此时没有事务操作该条记录,我们可以修改该条记录

  • 5.如果某个版本对当前事务不可见,那么顺着版本链找到下个版本记录,然后继续上面的对比规则,直到找到版本链中的最后一个版本,如果最后一个版本都不可见,那么该条记录对此事务完全不可见,也就查不到这个记录。

2.5 不同隔离级别使用Readview

  • 1.读未提交:能够读取未提交的事务修改的数据,所以直接读取最新的记录就可以,不必使用MVCC
  • 2.读已提交:不能读取未提交的事务修改的数据,并且不能进行重复读取,事务中,每次快照读都会新生成一个快照和ReadView,这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。
  • 3.可重复读:不能读取未提交的事务修改的数据,并且能进行重复读取,所以只在第一次查询的时候获取一次ReadView,之后查询都只查看已经生成的ReadView副本
  • 4.可串行化:InnoDB规定使用加锁的方式来访问记录,通过加锁的方式让所有sql都串行化执行了,也是读最新的,不存在快照读ReadView。

https://www.cnblogs.com/tod4/p/17384677.html

MySQL进阶系列:多版本并发控制mvcc的实现

2.6  mvcc解决幻读问题


MySQL在Repeatable Read隔离级别下是可以解决幻读问题的,解决的方案有两种:

  • 1.使用MVCC进行快照读,写使用临键锁。添加的临键锁不会影响快照读,只会影响到想要获取锁的读操作
  • 2.读写加锁,也就是使用可串行化的隔离模式。

2.6.1 mvcc解决幻读


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

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

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

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


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

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

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

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

MYDB中的MVCC


之前的实现中,我们知道数据管理器(DM)通过操作持久化的db文件,并通过封装,向上提供的抽象数据是DataItem,也就是说,上层看到的数据都是DataItem的形式 而本章讨论的版本管理器(VM)会进一步封装,给VM之上的模块提供一种抽象数据,即“记录”(Entry)。VM之上的模块看到的数据以及操作的数据都是Entry的形式 VM在内部,为每个Entry维护了多个版(Version)。当上层模块对某个“记录”(Entry)修改时,VM就创建一个新的版本 如上的MVCC策略能够降低不同操作之间互相阻塞的概率,例如实现可重复读等隔离级别。

对于一条记录来说,MYDB 使用 Entry 类维护了其结构。虽然理论上,MVCC 实现了多版本,但是在实现中,VM 并没有提供 Update 操作,对于字段的更新操作由后面的表和字段管理(TBM)实现。所以在 VM 的实现中,一条记录只有一个版本。 由于一条记录存储在一条 Data Item 中,所以 Entry 中保存一个 DataItem 的引用即可。

具体参看:https://blog.csdn.net/qq_64948664/article/details/141749439

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值