Skip to content

Mvcc机制

1. Mvcc简介

全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,即在同一时刻同一条记录在系统中可以存在多个版本。快照读为MySQL实现MVCC提供了一个非阻塞读功能。
在MySQL InnoDB中,MVCC的实现主要是为了提高数据库并发性能,它能很好地处理MySQL的读写冲突,做到尽量不加锁,大大降低系统的开销。MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log日志、Read View。

当一个事务操作更新一条记录后,会同时把当前事务ID更新到这条数据记录上(隐藏字段)。当下一个事务要操作这条记录时,会先比较trx_id和自己的事务ID大小,同时和自己维护的活跃事务列表ID比较,如果不能直接读取记录行的数据,那么会顺着undo日志的"链条"找到自己能用的数据版本,这就是MVCC。

2. 隐式字段

当我们创建表的时候,InnoDB还会自动的给我们添加三个隐藏字段及其含义分别是: Alt text 而上述的前两个字段是肯定会添加的,是否添加最后一个字段DB_ROW_ID,得看当前表有没有主键,如果有主键,则不会添加该隐藏字段。

3. undo日志

undo日志,也称回滚日志,它是InnoDB存储引擎在insert、update、delete的时候产生的便于数据回滚的日志。在数据更新之前,MySQL就需要先把更新前的数据记录到undo log日志中,当事务回滚时,可以利用undo log来进行回滚。作用包含两个——提供回滚、MVCC(多版本并发控制)。undo log主要分为两种:

  • insert undo log:顾名思义,此代表执行insert语句时产生的undo log, 它只在事务回滚时需要,因为这种log只是对本事务可见,其他事务不可见,所以当事务提交后,这种类型的undo log就会被系统直接删除回收(也就是该undo log占用的undo页面链表被释放)。
  • update undo log:事务在进行update或者delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要(也就是MVCC),所以不能在事务提交后马上删除,只在提交后放入undo log的链表,等待purge线程进行最后的删除。

4. Read View

Read View就是事务进行快照读操作的时候产生的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统数据以及当前活跃事务的ID(就是启动了还没提交的事务)。

4.1 当前读

使用到当前读的场景有select lock in share mode(共享锁)、 select for update 、 update、insert、delete(排他锁)等,这些操作都是一种当前读,因为它需要读取记录的最新版本,而且读取时还有可能会通过加锁保证其他事务不能同时修改当前记录。

4.2 快照读

快照读一般指不加锁的select操作,当然如果MySQL数据库的事务隔离级别是串行隔离级别,串行级别下的快照读会退化成当前读。

4.3 Read View组成

一个Read View包含以下四个字段:

  • creator_trx_id:指创建该Read View的事务的事务id。
  • m_ids:指创建该Read View时数据库中所有活跃事务的事务id列表,这是一个列表,活跃事务则代表是已启动但未提交的事务。
  • min_trx_id:指创建 Read View 时数据库中所有活跃事务中最小的事务id,也就是m_ids中的最小值。
  • max_trx_id:指下一个要创建Read View的事务的事务id,它并不是m_ids中的最大值,需要加以区分。

在可重复读隔离级别下,Read View是在事务开始(begin)之后且执行第一条sql时创建,创建Read View的同时也就生成了一个新的事务id(直到commit结束),事务会依赖该Read View保证查询结果保持不变直到该事务结束。

5. 基于多版本实现可重复读

MySQL的Innodb引擎借助Mvcc机制实现了可重复读。
假如现在Tom的账户余额有100,当前该记录上的事务id是10。
Alt text 现在MySQL系统有且只有两个活跃事务,两个事务同时在操作Tom的账户,分别为事务A和事务B。事务A在事务B前启动,但事务A的第一条sql执行前事务B也已启动。基于以上,事务A和事务B的Read View如下:
Alt text 为什么两个事务的m_ids都是[11, 12]?因为前面已经说过,RR隔离级别下的Read View是事务内第一个sql语句时才会创建。
在事务A的Read View中,它的事务id是11,由于是在事务B启动后才创建,所以此时活跃的事务id就有11和12,活跃的事务id中最小的事务id 11,下一个事务id是13。 在事务B的Read View 中,因为它后启动于事务A,所以它的事务id是12,当然此时活跃的事务的事务id有11和12,活跃的事务id中最小的事务id还是11,下一个事务id依然是13。
我先把事务A和事务B内部操作流程画出来:
Alt text 在时间线第③步,事务A查询Tom账户余额时,对应记录上的隐藏字段记录着事务id(trx_id)=10,通过和事务A自己本身的Read View的m_ids 字段对比可以发现,trx_id=10并不在活跃事务的列表中,并且事务A的事务id(11)比它还大,这就可以得出这条记录的事务(trx_id=10)已经在事务A启动前提交过了,所以该记录直接对事务A可见,事务A查询Tom账户余额是100。