文章目录
【MVCC概念】:MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。MVCC在MySQL InnoDB中的实现主要是为了提高数据库的并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
一、基础知识
1.当前读和快照读
- 当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。如下操作都属于当前读
select .....lock in share mode(共享锁)
select .....for update
update
insert
delete
(排他锁)
- 快照读
1.像不加锁的select操作就是快照读。
2.快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;
3.之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现就是基于多版本并发控制(MVCC),可以理解MVCC为行锁的一个变种,但在很多的情况下,避免了加锁操作,降低了开销。
4.既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
说白了,MVCC就是为了实现读和写冲突不加锁,而这个读指的就是快照读,而非当前读,当前读实际上是一种加锁的操作,是悲观锁的体现。
2.当前读、快照读和MVCC的关系
1.MVCC多版本并发控制是维持一个数据的多个版本,使得读写操作没有冲突的概念,只是一个抽象概念。
2.快照读就是MySQL实现MVCC理想模型中的其中一个非阻塞读功能。
3.MVCC模型在MySQL中的具体实现原则是由**3个隐式字段、undo日志、ReadView**等去完成的。
3. MVCC能解决什么问题,好处是?
数据库的并发场景有三种,分别为:
- 读-读:不存在问题,不需要并发控制;
- 读-写:有线程安全问题,可能遇到脏读、不可重复读、幻读;
- 写-写:有线程安全问题,可能存在更新丢失问题,比如第一类更新丢失,第二类更新丢失;
MVCC带来的好处是?
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制。
为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读事务开始前的数据库的快照。所以MVCC可为数据库解决一下问题:
1、 在并发读写数据库时,可做到在读操作时不用阻塞写操作,写操作时也不用阻塞读操作,提高了数据库并发读写性能;
2、同时解决了脏读、不可重复度等事务隔离问题(解决部分读写问题),但不能解决更新丢失的问题(写写冲突解决不了)。
【总结】:
为了不只让数据库采用悲观锁这样性能不佳的形式去解决并发问题,因此使用MVCC这样的非阻塞方式来解决问题,在数据库中,因为有了MVCC,所以我们可以形成两个组合:
1、MVCC+悲观锁
MVCC解决度读写冲突,悲观锁解决写写冲突;
2、MVCC+乐观锁
这种组合的方式,在最大程度上提升DB的并发性能,并解决读写冲突,和写写冲突导致的问题。
二、MVCC实现原理
其主要实现原理是依赖记录中的==3个隐式字段、undo日志、ReadView读视图==来实现,我们这三点来深入了解:
1、隐式字段
数据库表中的每行记录除了我们自定义字段外,还有数据库隐式定义的:
DB_TRX_ID:创建或者最后一次修改记录的事务ID
DB_ROW_ID:隐藏主键
DB_ROW_PTR:回滚指针(指向上一个历史记录) ----- 与undolog配合使用
例如下图所示:
2、undo log 回滚日志
主要分为两种:
-
insert undo log
代表事务在insert新记录时产生的undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃; -
update undo log
事务在进行update或dalete时产生的undo log;不仅在事务回滚时需要,在快照读时也需要,所以并不能随便删除。
对MVCC有帮助的实际是update undo log,undo log实际上就是保存数据的历史版本状态。
当不同的事务对同一条记录做修改时,会导致该记录的undo log形成一个链表,链表的首链是最新的历史记录,而链尾是最早的历史记录。
举个例子:
1、比如事务1插入person表一条新记录,记录如下:
2、现在来了个事务2对该记录的name做出了修改,改为lisi
- 在事务2修改该行数据时,数据库会先对该行进行加排它锁
- 然后把该数据拷贝到undo log中, 作为旧记录,即在undo log中有当前行的拷贝副本;
- 拷贝完毕后,name被改成了lisi; 隐藏字段:DB_TRX_ID:2 、DB_ROLL_PTR:指向uodolog地址的指针(历史记录)
- 事务提交后,释放锁;
3、事务3将age改为21
- 在事务3修改行数据时,数据库也先为该行加锁;
- 然后将该行数据拷贝进undo log中,作为旧记录;将最新的旧数据作为链表头,插在该行记录的undo log最前面
- 修改该行age为21,DB_TRX_ID:3;DB_ROLL_PTR:为刚拷贝到undo log的副本记录;
- 提交事务,释放锁
3、ReadView 读视图
事务在进行快照读时,产生的读视图。在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,此快照记录并维护当前活跃事务的ID(当每个事务开启时,都会被分配一个ID,这个ID是递增的,所以最新的事务,ID值越大)读视图中的字段:
- trx_list:系统活跃事务
- up_limit_id:活跃列表中最小的事务ID
- low_limit_id:尚未分配的下一个事务ID
所以,我们知道Read View主要是用来做可见性判断的,即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,即可能是当前的最新数据,也可能是该行记录的undo log里面的某个版本的数据。
Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(最后一次修改数据的事务ID)取出,与系统当前其他活跃事务的ID做对比(ReadView维护)。判断此DB_TRX_ID所在的旧记录当前事务是否能看见。
判断条件如下:
举个例子理解
MVCC的整体流程是怎么样的呢?我们来模拟理解一下:
1、事务0完成了数据的插入后,事务2对该数据执行了快照读,此时数据库为该行数据生成了一个ReadView读视图。此时还有事务1和事务3在活跃中,事务4在事务2快照读前一刻提交更新了,所以Read View记录了系统当前活跃事务1、3的ID,维护在一个列表trx_list
2、此时Read View中维护的数据为:
trx_list:1、2、3
up_limit_id:1
low_limit_id:5
3、事务4在事务2执行快照读前,提交了事务,所以我们的事务2在快照读该行记录的时候,就会拿该行记录的DB_TRX_ID去跟up_limit_id,low_limit_id,trx_list进行比较,判断当前事务2能看到的记录版本是哪个。
4、所以
1)先拿该记录DB_TRX_ID字段记录的事务ID:4去跟Read View的up_limit_id比较,看4是否小于up_limit_id:1;
2)如果不符合条件,继续判断4是否大于等于low_limit_id:5,也不符合条件;
3)最后判断4是否处于trx_list中的活跃事务,最后发现事务ID为4的事务不在当前活跃事务列表中,符合可见性条件------>因此事务2能读到的最新数据记录是事务4所提交的版本。
但如果是事务2在事务4提交之前,快照读过一次,则在事务4再提交一遍后,事务2快照读后是读不到事务4修改的最新版本的。
因为:
事务2第二次快照读的ReadView是沿用最初的
Db_TRX_ID是在活跃事务列表中,认为是还没有commit,所以经判断后,当前事务是看不到的。
这也正是Read View生成时机的不同,从而造成RC、RR级别下快照读的结果不同。