为什么需要MVCC
在没有MVCC之前,是使用读写锁(共享锁/排它锁)来进行并发控制的,读锁和读锁之间不互斥,写锁和读锁互斥,写锁和写锁互斥。
但是频繁加锁会导致数据库性能低下,这时出现了一种不加锁来解决读写冲突的方法,它会让数据库维护每条数据的多个版本,让不同的事务看到特定版本的数据,这个方法就是MVCC。
什么是MVCC
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种用来解决读写冲突的无锁并发控制的方法,可以提高数据库并发读写的性能。
什么是快照读和当前读
快照读
不加锁的select操作是快照读,快照读读到的数据可能是之前的历史版本。快照读的实现是基于MVCC。
当前读
像select lock in share mode(共享锁),select for update,update,insert,delete(排它锁)这些操作都是当前读,它读取的是记录的最新版本,读取时会对这些记录加锁,保证其他并发事务不能修改这些记录。
MVCC的实现原理
MVCC是通过隐式字段,undo log和read view来实现的。
隐式字段
InnoDB在表中每行记录的后面,都隐式记录了几个隐藏的字段:
- DB_ROW_ID:自增ID,如果数据表没有主键,InnoDB会以DB_ROW_ID来生成聚簇索引
- DB_TRX_ID:最近插入/修改事务ID,记录创建该记录/最后一次修改该记录的事务ID
- DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本
Undo Log
undo log就是为了实现回滚操作而记录的日志。undo log分为两种,分别为:
- insert undo log:在插入数据时产生,会记录这条数据的主键值,回滚时会将这个主键值对应的记录删掉。只在事务回滚时需要,所以在事务提交后会被立即丢弃。
- update undo log:在更新或删除时产生,会把修改这条记录前的旧值都记录下来,回滚时会将这个记录更新为旧值。不仅在事务回滚时需要,在快照读时也需要,所以不能随便删除;只有当日志不再会被用到时,才会被purge线程清除。
在MVCC中用到的是update undo log,它的结构如下:
Read View
read view就是事务进行快照读操作的时候生成的读视图(并不是每次快照读都会生成),read view主要包含以下四个属性:
- trx_list:活跃的事务ID列表;
- creator_trx_id:当前事务ID;
- up_limit_id:记录trx_list中最小的事务ID;
- low_limit_id:尚未分配的下一个事务ID。
当读取一条数据时,会先将这条数据的最新记录的DB_TRX_ID取出来,和read view中的属性进行可用性判断,如果不符合可见性的话,那就会通过DB_ROLL_PTR去undo log中取出上一个版本记录中的事务ID再进行可见性判断,以此类推,直到找到可见的记录为止。
可见性判断是使用可见性算法实现的,具体判断过程如下:
- 如果 DB_TRX_ID < up_limit_id 或 DB_TRX_ID == creator_trx_id,则该记录可见;
- 如果 DB_TRX_ID >= low_limit_id,则该记录不可见;
- 判断DB_TRX_ID是否在trx_list中;如果在,说明生成read view的时候,修改这个记录的事务还没提交,所以该记录不可见;如果不在trx_list中,说明生成read view之前修改这个记录的事务已经提交了,所以该记录可见。
事务隔离级别与MVCC的关系
READ UNCOMMITTED(读未提交)
READ UNCOMMITTED级别直接读取数据的最新记录,因此不会使用MVCC。
READ COMMITTED(读已提交)
READ COMMITTED级别会使用MVCC,在每次进行快照读都会生成新的read view,所以每次读取到的都是已提交的最新版本的记录,这样就可以解决脏读问题。
REPEATABLE READ(可重复读)
REPEATABLE READ级别会使用MVCC,只有在第一次进行快照读会生成read view,之后的快照读都会沿用第一次生成的read view,所以每次快照读读到的数据都是一样的,这样就可以解决脏读问题以及快照读的不可重复读、幻读问题。
RR级别下当前读的不可重复读问题
在快照读和当前读同时使用时,仍然有可能发生不可重复读的问题,需要使用行锁来解决。
RR级别下当前读的幻读问题
在快照读和当前读同时使用时,仍然有可能发生幻读的问题,需要使用next-key锁(行锁+gap锁)来解决。
SERIALIZABLE(串行化)
SERIALIZABLE级别是通过加锁来访问数据,因此不会使用MVCC。