浅析MySQL中的MVCC

120 阅读5分钟

浅析MySQL中的MVCC

前言

当前读与快照读

  • 当前读: 读取的记录是最新的版本,读取时要保证其他并发事物不能修改当前记录,会对记录的数据进行加锁
  • 快照读:出于对并发性能的考虑,快照读的现实是基于多版本并发控制,即MVCC。可以理解为行锁的抽象形式,通过维护多个版本而避免加锁的操作,即快照读到的并不一定是数据的最新版本,可能是历史版本。
  • MVCC:实现读-冲突不加锁,而对于当前读实际上是一种加锁操作,是悲观锁的实现。

MVCC

MVCC是一种并发控制机制,允许多个事务同时读取和写入数据库,不需要进行相互等待。为每次更新保存一个版本,版本与事务时间戳关联,读操作只读取该事物开始前的数据库的快照,使用无锁的方式来解决读-写冲突问题。

对于写操作:会创建新的数据版本,当未提交前,仍然读取的是历史版本记录,只有在事务提交以后,新版本的数据才会被其他事务看见,所以未提交事务的修改不会影响其他事务的读取。

对于读操作:数据库为每个事务创建一个数据快照,当数据被修改时,MySQL会生成新版本的记录,并且保留下来修改事务的信息,多个版本之间连接成一个版本链。当读取的时候可以进行判断选择合适的版本进行读取,同时也不会对写操作进行阻塞。

MVCC的实现

MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志,Read View来实现的。

隐藏字段:

每行记录除了我们自定义的字段外,还有数据库隐式定义的 DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID 等字段

DB_TRX_ID:最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID

DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里)

DB_ROW_ID:隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID产生一个聚簇索引

例如: e7690bf5a71906c2fff3f26f79c74d08.png

undo日志:

当有多个事务对该条数据进行修改以后,会保留之前版本的数据,将旧数据添加到新数据的末尾,导致该记录的undo log成为一条记录版本线性表,即链表, undo log 的链首就是最新的旧记录,链尾就是最早的旧记录。 90477f5263355f2151f033080f536ef3.png

Read View 读视图

Read View 就是事务进行快照读操作的时候生产的读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID。通过Read View来做可见性判断,将它作为条件来判断当前事务能够看到那个版本的数据,可能是新数据或是旧数据。

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 所在的旧记录就是当前事务能看见的满足条件的最新版本。

  • 首先比较 DB_TRX_ID < up_limit_id , 如果小于,则当前事务能看到 DB_TRX_ID 所在的记录,如果大于等于进入下一个判断
  • 判断 DB_TRX_ID >= low_limit_id , 如果大于等于则代表 DB_TRX_ID 所在的记录在 Read View 生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
  • 判断 DB_TRX_ID 是否在活跃事务之中,trx_list.contains (DB_TRX_ID),如果在,则代表我 Read View 生成时刻,你这个事务还在活跃,还没有 Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在 Read View 生成之前就已经 Commit 了,能够看到修改以后的结果

通过读视图可以可以找到符合条件的最新的数据版本进行读取。

RC , RR 级别下的快照读有什么不同?

Read View生成时机的不同,从而造成 RC , RR 级别下快照读的结果的不同. 在 RC 隔离级别下,是每个快照读都会生成并获取最新的 Read View;而在 RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View, 之后的快照读获取的都是同一个 Read View。