一致性的非锁定读( consistent nonlocking read)是指 InnoDB存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行 DELETE或 UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地, InnoDB存储引擎会去读取行的一个快照数据。如图所示。
图直观地展现了 InnoDB存储引擎一致性的非锁定读。之所以称其为非锁定读,因为不需要等待访问的行上X锁的释放。
快照数据是指该行的之前版本的数据,该实现是通过undo段来完成。而 undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。
可以看到,非锁定读机制极大地提高了数据库的并发性。在 InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。
通过图6-4可以知道,快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。就图6-4所显示的,一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi version Concurrency Control, MVCC)。
在事务隔离级别 READ COMMITTED和 REPEATABLE READ( InnoDB存储引擎的默认事务隔离级别)下, InnoDB存储引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同。在 READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。来看下面的一个例子,首先在当前 MySQL数据库的连接会话A中执行如下SQL语句:
#Session A
mysql> BEGIN;
Query OK,0 rows affected (0.00 sec)
mysql> SELECT * FROM parent WHERE id =1;
id
1
会话A中已通过显式地执行命令 BEGIN开启了一个事务,并读取了表 parent中id为1的数据,但是事务并没有结束。与此同时,用户再开启另一个会话B,这样可以模拟并发的情况,然后对会话B做如下的操作
#Session B
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE parent set id=3 WHERE id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
在会话B中将事务表 parent中i为1的记录修改为id=3,但是事务同样没有提交,这样id=1的行其实加了一个X锁。这时如果在会话A中再次读取id为1的记录,根据 InnoDB存储引擎的特性,即在 READ COMMITTED和 REPEATETABLE READ的事务隔离级别下会使用非锁定的一致性读。回到之前的会话A,接着上次未提交的事务,执行SQL语句 SELECT* FROM parent WHERE id=1的操作,这时不管使用READ COMMITTED还是 REPEATABLE READ的事务隔离级别,显示的数据应该都是:
#Session A
mysql> SELECT FROM parent WHERE id = 1;
id
1
1 row in set (0.00 sec)
由于当前id=1的数据被修改了1次,因此只有一个行版本的记录。接着,在会话B中提交上次的事务:
# Session B
mysql> commit
Query OK, 0 rows affected (0.01 sec)
在会话B提交事务后,这时在会话A中再运行 SELECT * FROM parent WHERE id=1的SQL语句,在READ COMMITTED和REPEATABLE事务隔离级别下得到结果就不一样了。对于READ COMMITTED的事务隔离级别,它总是读取行的最新版本,如果行被锁定了,则读取该行版本的最新一个快照(fresh snapshot)。在上述例子中,因为会话B已经提交了事务,所以READ COMMITTED事务隔离级别下会得到如下结果:
mysql> SELECT @@tx_isolation\G;
******************1.row********************
@@tx_isolation: READ-COMMITTED
l row in set (0.00 sec)
mysql> SELECT FROM parent WHERE id =1;
Empty set (0.00 sec)
而对于 REPEATABLE READ的事务隔离级别,总是读取事务开始时的行数据。因此对于REPEATABLE READ事务隔离级别,其得到的结果如下:
mysql> SELECT @@tx_isolation\G;
******************1.row********************
@@tx_isolation:: REPEATABLE-READ
1 row in set (0.00 sec)
mysql> SELECT FROM parent WHERE id = 1;
id
1
1 row in set (0.00 sec)
下面将从时间的角度展现上述演示的示例过程,如下表所示。需要特别注意的是,对于READ COMMITTED的事务隔离级别而言,从数据库理论的角度来看,其违反了事务ACID中的I的特性,即隔离性。
时间
会话A
会话B
时间
会话A
会话B
1
BEGIN
2
SELECT*FROMparentWHEREid=1
3
BEGIN
4
UPDATEparentSETid=3WHEREid=1
5
SELECT*FROMparentWHEREid=1
6
COMMIT
7
SELECT*FROMparentWHERE id=1;
8
COMMIT