一、概述
1、MVCC
Multi-Version Concurrency Control, 即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
2、多版本控制
- 是一种提高并发的技术
- 在数据库中,只有读读之间可以并发执行,读写,写读,写写都要堵塞
- 引入多版本后,只有写写之间需要堵塞,其他三种操作都可以并发执行,提高了InnoDB的并发度。
二、当前读和快照读
1、当前读
-
读取该记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
-
形式:
- select lock in share mode(共享锁)
- select * for update
- insert
- update
- delete
2、快照读
- 不加锁的select操作就是快照读,即不加锁的非堵塞读
- 快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读
- 快照读的实现是基于MVCC,快照读可能读到的并不一定是数据的最新版本,可能是历史版本数据。
3、当前读、快照读、MVCC的关系
- MVCC多版本并发控制指的是“维持一个数据的多个版本,使得读写操作没有冲突”,这仅是一个理论。
- 而在MySql中,实现一个MVCC理论,就是使用快照读的方式实现了该理论。
- 当前读使用了悲观锁,快照读使用了乐观锁
三、MVCC实现原理
1、隐式字段
每行记录中除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID、DB_ROLL_PRT、DB_ROW_ID:
-
DB_TRX_ID:6byte,最近修改(修改|插入)的事务ID,记录创建这条记录、最后一次修改该记录的事务ID
-
DB_ROLL_PRT:7byte,回滚指针,指向这条记录的上一个版本,存储在rollback segment中
-
DB_ROW_ID:6byte,隐含的自增ID(隐藏主键),如果数据表没有设置主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
2、undo日志
2.1、日志类型
- insert undo log:代表事务在insert新纪录时产生的undo log,只有事务回滚时需要,并且在事务提交后可以被立即丢弃。
- update undo log:代表事务在update或delete 时产生的undo log,不仅在事务回滚时需要,在快照读时也需要,不能随便删除。
2.2、undo log流程
A.一开始有个事务往persion表插入了一条新纪录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL。
B.现在有个事务1,对该记录的name做出了修改,改为Tom
- 在事务1修改该记录数据时,数据库会先对该行加排他锁,防止别人修改。
- 然后把该行数据拷贝到undo log中,作为旧记录,即在undo log中有当前行的数据副本。
- 拷贝完毕后,修改该数据name为Tom,并且修改隐藏字段:DB_TRX_ID=1,回滚指针指向undo log中的副本记录,即表示我的上一个版本就是 它。
- 事务提交后,释放该记录上的排他锁。
C、又来个事务2,对该记录的age做出了修改,改为30岁
重复事务1的操作
2.3、总结
- 不同事务或者相同事务的对同一记录的修改,会导致该记录的
undo log成为一条记录版本线性表,既链表。 undo log的链首就是最新的旧记录,链尾就是最早的旧记录。
3、Read View(读视图)
3.1、概述
- 事务进行快照读操作的时候生产的读视图,在该事务执行快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID
- 每个事务开启时,都会被分配一个ID,递增的,所以最新的事务,ID值越大
- Read View主要用来做可见性判断的,即当某个事务执行快照读是,对该记录创建一个Read View读视图,把它比作条件判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也可能是该记录的undo log记录中的历史数据。
3.2、可见性算法
-
数据
- trx_id:undo log该行数据的隐藏字段DB_TRX_ID
- m_ids:当前快照读中所有活跃(未提交的)事务ID集合
- min_trx_id:当前活跃事务中的最小的事务ID
- max_trx_id:下一个将要分配的事务ID
- creator_trx_id:当前快照读的事务ID
-
是否可见
- trx_id == creator_trx_id: 可以访问该数据,表示该数据由当前事务创建或修改
- trx_id < min_trx_id : 可以放访问该数据,min_trx_id是最小的活跃事务ID,trx_id小于min_trx_id,表示该事务已提交,所以可以读取已提交的数据
- trx_id > max_trx_id : 不可以访问该数据,max_trx_id是将来分配的事务ID,trx_id大于max_trx_id, 表示该事务是在别的事务进程中提交的,隔离级别RR,解决了可重复读问题,不能在同一事务中读取到不同数据。
- min_trx_id <= trx_id <= max_trx_id : 如果trx_id在m_ids集合中,不可以访问该版本数据,反之可以。 因为此时trx_id属于活跃事务,未提交,所以不能读取未提交的数据,反之trx_id不在m_ids中,说明是已经提交的事务,可以读取已提交数据。
4、整体流程
1、当前开始一个事务ID=2,此时还有事务1和事务3在活跃中,而事务4在事务2开启前提交更新了
- m_ids:1, 3
- min_trx_id:1
- max_trx_id: 4+1=5
- creator_trx_id:2
2、在事务2开启时,生成了一个Read View,该视图记录了当前系统活跃的事务1,2,还有已提交的事务4
3、根据可见性算法判断
- trx_id == creator_trx_id ?4 不等于 2,不满足条件,读取不了事务4已提交的数据
- trx_id < min_trx_id ? 4 不小于1,不满足条件,读取不了事务4提交的数据
- trx_id > max_trx_id ? 4 不大于5,不满足条件,读取不了事务4提交的数据
- min_trx_id <= trx_id <= max_trx_id ? 1 <= 4 <=5 ,并且4不在m_ids中,说明不是一个活跃的事务ID,即该事务已提交,则可以读取事务4已提交的数据
四、MVCC相关问题
1、RR是如何在RC级的基础上解决不可重复读的?
事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读的地方非常关键,它有决定该事务后续快照读结果的能力
Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同:
- 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;
- 即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
- 而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因
总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。