MySQL MVCC
MVCC(Multi-Version Concurrency Control),多版本并发控制。
MVCC是一种并发控制的方法,通过维护数据 多个版本的记录,以无锁的方式解决并发读写冲突。目的就是规避在读写冲突的时候进行加锁的操作。Mysql的Innodb引擎就使用了MVCC。

相关概念

要了解MVCC的实现机制,需要先知道Mysql中以下几个概念。

事物ID和DB_TRX_ID

事物ID

我们都知道innodb是支持事物的,在innodb中每一个事物创建时都会分配一个自增的ID作为事物为唯一标志,也就是事物ID。

DB_TRX_ID

数据表里每一行数据都会有一个隐藏字段DB_TRX_ID,用来存储创建或者最后一次修改此记录的事物ID

undo log和DB_ROLL_PTR

DB_ROLL_PTR

数据表里另外一个隐藏字段,DB_ROLL_PTR回滚指针,指向这条记录的上一个版本在undo log中的数据。

undo log

undo log存储每行记录的修改历史。可以简单理解undo log是一个与数据表结构相同的另外一张表,数据表的数据行字段它都有,数据表的行记录每修改一次,就将这行数据的当前记录写到undo log中,并将返回的undo log指针地址写入DB_ROLL_PTR,然后修改数据行数据、更新事物ID。
undo log主要分为两种:

insert undo log

代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃

update undo log

事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
结构大概如下图所示:
2021-08-26-13-58-41-271293.png
undo log可以作为mvcc中查找对应可读记录,也可以作为当前事物的rollback依据。
undo log也并不是无限增长的,会有另外一个线程会尝试清除早期的undo log记录,因为他们已经没有用处了。

当前读与快照读

当前读

当前读指的是读取数据当前最新数据。updateinsertdeleteselect for update(排他锁)、select lock in share mode。读取数据需要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

快照读

快照读指的是在读取数据时,生成读取快照,在同一个事物中可能会一直读取此快照的数据。快照读读到的数据可能不是最新的,可能是历史版本的数据,这些历史版本的数据就是从undo log中获取的。
事物中的select 不加锁的情况会执行快照读,快照读依赖readview来实现。
快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。

ReadView

读视图,由当前活跃事物ID的列表trx_list、当前活跃最小事物ID low_limit_id、下一个即将分配的事物ID up_limit_id,三个部分组成。

事物间可见性分析

基于ReadView的可见性分析逻辑

执行快照读的时候,会创建一个ReadView。定义被读取行的DB_TRX_ID 为trx_id

  1. 比较 trx_id是否小于low_limit_id或者为当前事物ID,如果为true,则代表修改此行数据的事物早已提交或者就是当前事务进行的修改,当前记录可见。否则进入下一步判断。
  2. 比较trx_id是否大于等于up_limit_id,如果为true,则代表修改此行记录的事物晚于当前读视图创建,当前记录不可见,根据DB_ROLL_PTR undo log指针找到上一条记录,从新进行可见性分析。否则进入下一步判断
  3. 判断trx_id是否在trx_list列表中,如果在,代表修改此行记录的事物还未提交,当前事务不可以读取当前记录,根据DB_ROLL_PTR undo log指针找到上一条记录,从新进行可见性分析。否则说明数据在readview生成的时候已经提交,当期事物可以读取当前记录。

    数据库事物隔离级别与MVCC

  • 脏读
    读到了别的事物未提交的数据,由于别的事物有可能会回滚,相当于读到了错误的数据。
  • 不可重复读
    读到了别的事物已提交的数据,在当前事务中,前后两次的读取可能数据不一致。
  • 幻读
    也是读到了别的事物已提交的数据,在当前事务中,前后两次的读取可能数据不一致。与不重复读区别是,幻读指的是insert或delete产生的不一致,而不可重读指的是update产生的不一致。

数据库为了解决脏读、不可重复读、幻读,定义了事物间的隔离级别。

事物隔离级别

  • 串行化Serializable
    一切指令同步执行,也就没有以上的问题了。可以解决脏读、幻读、不可重读。
  • 可重复读Repeat Read、RR
    在同一事物中读取被修改的记录,总是一致的。可以解决脏读、不可重复读。
  • 读已提交Read Committed、RC
    可以读取别的事物已经提交的数据。可以解决脏读。
  • 读未提交Read UnCommitted
    可以读到别的事物未提交的数据。啥问题都没解决。

隔离级别是约严格需要的约束越多,相对的性能就会越差。
Mysql Innodb的默认数据库隔离级别为RR。
Oracle的隔离级别只支持Serializable和RC,另外提供了一种只读的模式,只允许select。

事物隔离级别与MVCC

在RR级别下,事物进行快照读时会检查当前事物是否已经创建过ReadView,如果存在,则使用已经创建的,这也是实现可重复的方法。
在RC级别下,事物每一次快照读都会创建一个新的ReadView,这样就会造成不可重复的和幻读的问题。
Innodb利用MVCC解决了RR级别下快照读中的幻读问题,当前读中的幻读问题需要使用GAP lock解决,也就是间隙锁。
举个例子:
创建user表,自增主键ID、name、age,三个字段;
插入两条数据1-张三-10、2-李四-20;
2021-08-26-13-58-41-364292.jpg
启动事物1,查询name=张三的记录,会返回1-张三;
启动事物2,新增记录3-张三-30;
在事物1中再次查询name=张三的记录,仍然返回1-张三;
在事物1中修改name=张三的age=45,提交;会发现1、3的age都会变为45。
2021-08-26-13-58-41-420290.jpg
这就是解决了快照读的幻读,而修改操作属于当前读,仍然有幻读的问题。
但是如果这里将事物2的提交事务延迟到事物1修改数据之后,会发现事物1的修改数据会被卡住。
这里就使用了间隙锁进行防止写的幻读。