一致性非锁定读(consistent nonlocking read)是指InnoDB存储引擎经过多版本控制(MVVC)读取当前数据库中行数据的方式。若是读取的行正在执行DELETE或UPDATE操做,这时读取操做不会所以去等待行上锁的释放。相反地,InnoDB会去读取行的一个快照。mysql
上图直观地展示了InnoDB一致性非锁定读的机制。之因此称其为非锁定读,是由于不须要等待行上排他锁的释放。快照数据是指该行的以前版本的数据,每行记录可能有多个版本,通常称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control, MVVC)。InnoDB是经过undo log来实现MVVC。undo log自己用来在事务中回滚数据,所以快照数据自己是没有额外开销。此外,读取快照数据是不须要上锁的,由于没有事务须要对历史的数据进行修改操做。git
一致性非锁定读是InnoDB默认的读取方式,即读取不会占用和等待行上的锁。可是并非在每一个事务隔离级别下都是采用此种方式。此外,即便都是使用一致性非锁定读,可是对于快照数据的定义也各不相同。github
在事务隔离级别READ COMMITTED和REPEATABLE READ下,InnoDB使用一致性非锁定读。然而,对于快照数据的定义却不一样。在READ COMMITTED事务隔离级别下,一致性非锁定读老是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,则读取事务开始时的行数据版本。sql
咱们下面举个例子来详细说明一下上述的状况。数据库
# session A mysql> BEGIN; mysql> SELECT * FROM test WHERE id = 1;
咱们首先在会话A中显示地开启一个事务,而后读取test表中的id为1的数据,可是事务并无结束。于此同时,用户在开启另外一个会话B,这样能够模拟并发的操做,而后对会话B作出以下的操做:数组
# session B mysql> BEGIN; mysql> UPDATE test SET id = 3 WHERE id = 1;
在会话B的事务中,将test表中id为1的记录修改成id=3,可是事务一样也没有提交,这样id=1的行其实加了一个排他锁。因为InnoDB在READ COMMITTED和REPEATABLE READ事务隔离级别下使用一致性非锁定读,这时若是会话A再次读取id为1的记录,仍然可以读取到相同的数据。此时,READ COMMITTED和REPEATABLE READ事务隔离级别没有任何区别。session
如上图所示,当会话B提交事务后,会话A再次运行SELECT * FROM test WHERE id = 1
的SQL语句时,两个事务隔离级别下获得的结果就不同了。
对于READ COMMITTED的事务隔离级别,它老是读取行的最新版本,若是行被锁定了,则读取该行版本的最新一个快照。由于会话B的事务已经提交,因此在该隔离级别下上述SQL语句的结果集是空的。
对于REPEATABLE READ的事务隔离级别,老是读取事务开始时的行数据,所以,在该隔离级别下,上述SQL语句仍然会得到相同的数据。数据结构
咱们首先来看一下wiki上对MVVC的定义:并发
Multiversion concurrency control (MCC or MVCC), is a concurrency control
method commonly used by database management systems to provide
concurrent access to the database and in programming languages to
implement transactional memory.
由定义可知,MVVC是用于数据库提供并发访问控制的并发控制技术。
数据库的并发控制机制有不少,最为常见的就是锁机制。锁机制通常会给竞争资源加锁,阻塞读或者写操做来解决事务之间的竞争条件,最终保证事务的可串行化。而MVVC则引入了另一种并发控制,它让读写操做互不阻塞,每个写操做都会建立一个新版本的数据,读操做会从有限多个版本的数据中挑选一个最合适的结果直接返回,由此解决了事务的竞争条件。
考虑一个现实场景。管理者要查询全部用户的存款总额,假设除了用户A和用户B以外,其余用户的存款总额都为0,A、B用户各有存款1000,因此全部用户的存款总额为2000。可是在查询过程当中,用户A会向用户B进行转帐操做。转帐操做和查询总额操做的时序图以下图所示。mvc
若是没有任何的并发控制机制,查询总额事务先读取了用户A的帐户存款,而后转帐事务改变了用户A和用户B的帐户存款,最后查询总额事务继续读取了转帐后的用户B的帐号存款,致使最终统计的存款总额多了100元,发生错误。
使用锁机制能够解决上述的问题。查询总额事务会对读取的行加锁,等到操做结束后再释放全部行上的锁。由于用户A的存款被锁,致使转帐操做被阻塞,直到查询总额事务提交并将全部锁都释放。
可是这时可能会引入新的问题,当转帐操做是从用户B向用户A进行转帐时会致使死锁。转帐事务会先锁住用户B的数据,等待用户A数据上的锁,可是查询总额的事务却先锁住了用户A数据,等待用户B的数据上的锁。
使用MVVC机制也能够解决这个问题。查询总额事务先读取了用户A的帐户存款,而后转帐事务会修改用户A和用户B帐户存款,查询总额事务读取用户B存款时不会读取转帐事务修改后的数据,而是读取本事务开始时的数据副本(在REPEATABLE READ隔离等级下)。
MVCC使得数据库读不会对数据加锁,普通的SELECT请求不会加锁,提升了数据库的并发处理能力。借助MVCC,数据库能够实现READ COMMITTED,REPEATABLE READ等隔离级别,用户能够查看当前数据的前一个或者前几个历史版本,保证了ACID中的I特性(隔离性)
多版本并发控制仅仅是一种技术概念,并无统一的实现标准, 其的核心理念就是数据快照,不一样的事务访问不一样版本的数据快照,从而实现不一样的事务隔离级别。虽然字面上是说具备多个版本的数据快照,但这并不意味着数据库必须拷贝数据,保存多份数据文件,这样会浪费大量的存储空间。InnoDB经过事务的undo日志巧妙地实现了多版本的数据快照。
数据库的事务有时须要进行回滚操做,这时就须要对以前的操做进行undo。所以,在对数据进行修改时,InnoDB会产生undo log。当事务须要进行回滚时,InnoDB能够利用这些undo log将数据回滚到修改以前的样子。
根据行为的不一样 undo log 分为两种 insert undo log和update undo log。
insert undo log 是在 insert 操做中产生的 undo log。由于 insert 操做的记录只对事务自己可见,对于其它事务此记录是不可见的,因此 insert undo log 能够在事务提交后直接删除而不须要进行 purge 操做。
update undo log 是 update 或 delete 操做中产生的 undo log,由于会对已经存在的记录产生影响,为了提供 MVCC机制,所以 update undo log 不能在事务提交时就进行删除,而是将事务提交时放到入 history list 上,等待 purge 线程进行最后的删除操做。
为了保证事务并发操做时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undo log的并发写入和持久化。回滚段其实是一种 Undo 文件组织方式。
InnoDB行记录有三个隐藏字段:分别对应该行的rowid、事务号db_trx_id和回滚指针db_roll_ptr,其中db_trx_id表示最近修改的事务的id,db_roll_ptr指向回滚段中的undo log。以下图所示。
当事务2使用UPDATE语句修改该行数据时,会首先使用排他锁锁定改行,将该行当前的值复制到undo log中,而后再真正地修改当前行的值,最后填写事务ID,使用回滚指针指向undo log中修改前的行。以下图所示。
当事务3进行修改与事务2的处理过程相似,以下图所示。
REPEATABLE READ隔离级别下事务开始后使用MVVC机制进行读取时,会将当时活动的事务id记录下来,记录到Read View中。READ COMMITTED隔离级别下则是每次读取时都建立一个新的Read View。
Read View是InnoDB中用于判断记录可见性的数据结构,记录了一些用于判断可见性的属性。
Read View建立后,事务再次进行读操做时比较记录的db_trx_id和Read View中的low_limit_id,up_limit_id和读写事务数组来判断可见性。
若是该行中的db_trx_id等于当前事务id,说明是事务内部发生的更改,直接返回该行数据。不然的话,若是db_trx_id小于up_limit_id,说明是事务开始前的修改,则该记录对当前Read View是可见的,直接返回该行数据。
若是db_trx_id大于或者等于low_limit_id,则该记录对于该Read View必定是不可见的。若是db_trx_id位于[up_limit_id, low_limit_id)范围内,须要在活跃读写事务数组(rw_trx_ids)中查找db_trx_id是否存在,若是存在,记录对于当前Read View是不可见的。
若是记录对于Read View不可见,须要经过记录的DB_ROLL_PTR指针遍历undo log,构造对当前Read View可见版本数据。
简单来讲,Read View记录读开始时及其以后,全部的活动事务,这些事务所作的修改对于Read View是不可见的。除此以外,全部其余的小于建立Read View的事务号的全部记录都可见。
咱们后续还会学习InnoDB的锁的相关的知识,请你们持续关注。