MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC。
在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。
在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?以MySQL InnoDB为例:
快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)
select * from table where ?;
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
select * from table where ? lock in share mode; select * from table where ? for update; insert into table values (…); update table set ? where ?; delete from table where ?;
所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。
InnoDB 的 MVCC 是通过在每行记录后面保存两个隐藏列来实现的。这两个列,一个是创建时间列, 保存行的 insert 时间或者 update 时间,一个是删除时间列, 保存行的 delete 时间。当然这个时间并不是实际的时间值,而是系统版本号(system version number, svn)。每开始一个新的事务,系统版本号都对自动递增。事务开始时刻的系统版本号会作为当前事务的版本号,用来和查询到的每行记录的版本号进行比较。
下面看一下在 REPEATABLE READ 隔离级别下,MVCC 具体是如何操作的。
SELECT
InnoDB 会根据以下两个条件检查每行记录:
只有同时符合上述两个条件的记录,才能返回作为查询结果
INSERT
InnoDB 保存当前事务的版本号到新插入行的创建时间列
DELETE
InnoDB 保存当前事务的版本号到删除行的删除时间列
UPDATE
InnoDB 插入一条新记录, 保存当前事务版本号到创建时间列,同时保存当前事务版本号到旧行的删除时间列
保存这个额外的系统版本号,使大多数读操作都可以不用加锁, non-blocking reads 就是这么实现的。这样设计使得读操作很简单,性能很好,并且保证只会读取到符合条件的行。不足之处是每行记录都要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作
值得注意的是,在事务隔离级别READCOMMITTED和REPEATABLEREAD(InnoDB存储引擎的默认事务隔离级别)下,InnoDB存储引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同。在READCOMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一 份快照数据。而在REPEATABLEREAD事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。