一、MVCC介绍
在早期的数据库中,只有读读之间的操作才可以并发执行,读写,写读,写写操作都要阻塞,这样就会导致MySQL的并发性能极差。
采用了MVCC机制后,只有写写之间相互阻塞,其他三种操作都可以并行,这样就可以提高了MySQL的并发性能。
二、MVCC生效前提
- MySQL使用InnoDB引擎。
- 事务隔离级别设置为READ COMMITTED或REPEATABLE READ。
三、MVCC依赖的数据结构
- 隐式字段
- DB_ROW_ID:隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引。
- DB_TRX_ID:最近修改(修改/插入)事务ID,记录创建这条记录/最后一次修改该记录的事务
- IDDB_ROLL_PTR:回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
- DELETED_BIT:记录被更新或删除并不代表真的删除,而是删除flag变了。
- undo log
针对数据库中的一条记录的每次修改都会有一条修改记录,方便事务的回滚,undo log主要氛围以下三种。- Insert undo log :插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉。
- Update undo log:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值。
- Delete undo log:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。
删除细节:删除操作都只是设置一下老记录的DELETED_BIT,并不真正将过时的记录删除。为了节省磁盘空间,InnoDB有专门的purge线程来清理DELETED_BIT为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的DELETED_BIT为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
我们以update表中的记录为例,查看一下undo log记录的流程。
表中已存在的记录如下
现在来了一个事务1对该记录的name做出了修改,改为Tom在事务
- 修改该行(记录)数据时,数据库会先对该行加排他锁
- 然后把该行数据拷贝到undo log中,作为旧记录,即在undo log中有当前行的拷贝副本
- 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,即表示我的上一个版本就是它
- 事务提交后,释放锁
-
ReadView
什么是Read View,说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。所以我们知道 Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,即可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本。
那么这个判断条件是什么呢?在展示之前,我先简化一下Read View,我们可以把Read View简单的理解成有三个全局属性。- trx_list 未提交事务ID列表,用来维护Read View生成时刻系统正活跃的事务ID
- up_limit_id 记录trx_list列表中事务ID最小的ID
- low_limit_id ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
判断是否可见算法(本事务要查看的数据为DB_TRX_ID)
- 首先比较DB_TRX_ID < up_limit_id,如果小于,则当前事务能看到DB_TRX_ID所在的记录,如果大于等于进入下一个判断(如果DB_TRX_ID为当前事务,则可见)。
- 接下来判断DB_TRX_ID大于等于low_limit_id,如果大于等于则代表DB_TRX_ID所在的记录在ReadView生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
- 判断DB_TRX_ID是否在活跃事务之中,trx_list.contains(DB_TRX_ID),如果在,则代表我ReadView生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在ReadView生成之前就已经Commit了,你修改的结果,我当前事务是能看见的。
RR和RC中ReadView生成规则
- RR中的ReadView已经事务开启,事务全局会共享一个
- RC中的ReadView事务开启后所有事务共享一个最新的
原文链接:https://pdai.tech/md/db/sql-mysql/sql-mysql-mvcc.html