聊聊MVCC和Next-key Locks

12,607 阅读9分钟

前言

上篇文章讲到了MySQL的RR隔离级别通过MVCC+Next-key Locks解决幻读问题,下面就给大家仔细讲讲这两个机制究竟是什么。

MVCC(多版本并发控制)

多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

Mysql的大多数事务型存储引擎实现都不是简单的行级锁。基于提升并发性考虑,一般都同时实现了多版本并发控制(MVCC),包括Oracle、PostgreSQL。不过实现各不相同。

MVCC的实现是通过保存数据在某一个时间点快照来实现的。也就是说不管实现时间多长,每个事物看到的数据都是一致的。

分为乐观(optimistic)并发控制和悲观(pressimistic)并发控制。

MVCC是如何工作的:

InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现。这两个列一个保存了行的创建时间,一个保存行的过期时间(删除时间)。当然存储的并不是真实的时间而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动新增。事务开始时刻的系统版本号会作为事务的版本号,用来查询到每行记录的版本号进行比较。

版本号

系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
事务版本号:事务开始时的系统版本号。

隐藏的列

MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
  • 创建版本号:创建一行数据时,将当前系统版本号作为创建版本号赋值。
  • 删除版本号:删除一行数据时,将当前系统版本号作为删除版本号赋值。如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。

REPEATABLE READ(可重复读)隔离级别下MVCC如何工作:

当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。

1. SELECT

InnoDB会根据以下条件检查每一行记录:

1. InnoDB只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行要么是在开始事务之前已经存在要么是事务自身插入或者修改过的,在事务开始之后才插入的行,事务不会看到。

2. 行的删除版本号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行在事务开始之前未被删除,在事务开始之前就已经过期的数据行,该事务也不会看到。
只有符合上述两个条件的才会被查询出来

2. INSERT

将当前系统版本号作为数据行快照的创建版本号。

3. DELETE

将当前系统版本号作为数据行快照的删除版本号。

4. UPDATE

将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行 DELETE 后执行 INSERT

保存这两个版本号,使大多数操作都不用加锁。使数据操作简单,性能很好,并且能保证只会读取到复合要求的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作和一些额外的维护工作。


MVCC只在COMMITTED READ(读提交)和REPEATABLE READ(可重复读)两种隔离级别下工作。

可以认为MVCC是行级锁一个变种,但是他很多情况下避免了加锁操作,开销更低。虽然不同数据库的实现机制有所不同,但大都实现了非阻塞的读操作(读不用加锁,且能避免出现不可重复读和幻读),写操作也只锁定必要的行(写必须加锁,否则不同事务并发写会导致数据不一致)。

快照读与当前读

在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,不是数据库最新的数据。这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库最新版本数据的方式,叫当前读 (current read)

1. 快照读

当执行select操作是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。快照的生成当在第一次执行select的时候,也就是说假设当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据。之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的。

使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销。

select * from table ...;

2. 当前读

对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。

读取的是最新的数据,需要加锁。以下第一个语句需要加 S 锁,其它都需要加 X 锁。

select * from table where ? lock in share mode; 
select * from table where ? for update; 
insert; 
update; 
delete;

如何解决幻读

很明显可重复读的隔离级别没有办法彻底的解决幻读的问题,如果需要解决幻读的话也有两个办法:
  • 使用串行化读的隔离级别
  • MVCC+next-key locks:next-key locks由record locks(索引加锁) 和 gap locks(间隙锁,每次锁住的不光是需要使用的数据,还会锁住这些数据附近的数据)

InnoDB有三种行锁的算法:

1,Record Lock:单个行记录上的锁。
2,Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
3,Next-Key Lock:1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

Record Locks

锁定一个记录上的索引,而不是记录本身。

如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。

Gap Locks

锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。

SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;

Next-Key Locks

Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。

MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。

当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。

它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。

新建一张表:

CREATE TABLE `test` ( 
`id` int(11) primary key auto_increment, 
`xid` int, KEY `xid` (`xid`) ) 
ENGINE=InnoDB DEFAULT CHARSET=utf8; 
insert into test(xid) values (1), (3), (5), (8), (11);
注意,这里xid上是有索引的,因为该算法总是会去锁住索引记录。
现在,该索引可能被锁住的范围如下:
(-∞, 1], (1, 3], (3, 5], (5, 8], (8, 11], (11, +∞)
根据下面的方式开启事务执行SQL:


Session A执行后会锁住的范围:
(5, 8], (8, 11]
除了锁住8所在的范围,还会锁住下一个范围,所谓Next-Key。
这样,Session B执行到第六步会阻塞,跳过第六步不执行,第七步也会阻塞,但是并不阻塞第八步,第九步也不阻塞。
上面的结果似乎并不符合预期,因为11这个值看起来就是在(8, 11]区间里,而5这个值并不在(5, 8]这个区间里。

看下图就明白了:


该SQL语句锁定的范围是(5,8],下个键值范围是(8,11],所以插入5~11之间的值的时候都会被锁定,要求等待。即:插入5,6,7,8,9,10 会被锁住。插入非这个范围内的值都正常。

小结

本篇文章总结了MVCC和InnoDB下的三种行锁的算法,这些知识属于MySQL的原理层面,有了这方面的认识后,在以后对MySQL的使用也能更加得心应手,不过我个人而言对于上面最后一个问题为什么xid为11时并不会被阻塞那里还有一点点不理解,参考的别人博客给出的解释是id是自增的,innodb的B+树是有序的,所以并不会阻塞后面的插入。此解释还有待我回去翻看一下《mysq技术内幕》中对于next-key locks的详细实现的描述再来做出更合理的解释。


参考自