聊聊MVCC和Next-key Locks

前言

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

MVCC(多版本并发控制)

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

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

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

分为乐观(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是行级锁一个变种,可是他不少状况下避免了加锁操做,开销更低。虽然不一样数据库的实现机制有所不一样,但大都实现了非阻塞的读操做(读不用加锁,且能避免出现不可重复读和幻读),写操做也只锁定必要的行(写必须加锁,不然不一样事务并发写会致使数据不一致)。ui

快照读与当前读

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

1. 快照读

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

使用 MVCC 读取的是快照中的数据,这样能够减小加锁所带来的开销。spa

select * from table ...;

2. 当前读

对于会对数据修改的操做(update、insert、delete)都是采用当前读的模式。在执行这几个操做时会读取最新的记录,即便是别的事务提交的数据也能够查询到。假设要update一条记录,可是在另外一个事务中已经delete掉这条数据而且commit了,若是update就会产生冲突,因此在update的时候须要知道最新的数据。

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

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的详细实现的描述再来作出更合理的解释。


参考自
相关文章
相关标签/搜索