15.7.2.3 一致性非锁定读

所谓一致性非锁定读,是指InnoDB使用了多版本技术,使得查询到的是数据库在某一时刻的快照。我们只能看到到这一时刻之前提交的事务产生的变更, 无法看到之后提交或未提交的事务产生的变更。 但是有一个例外:在同一个事务内,我们可以查询到之前的语句产生的变更。这个例外导致:如果你更 新了表中的某些行,再进行SELECT时你会看到被更新记录的最新版本,但是也可能看到任意记录的老版本。如果同时有其它会话在更新同一张表中的记录 ,这个异常意味着某一时刻你看到的这张表的状态从来没有在数据库中存在过。

如果事务隔离级别是PEPEATABLE READ(默认级别),同一个事务内的所有一致性读,读到的都是同一个快照。这个快照是在这个事务内第一次执行一致性 读时建立的。如果你想看到一个更新的快照,就要提交当前事务,然后发起新的查询。

如果隔离级别是READ COMMITTED,事务内的每次一致性读都会建立一个快照。

一致性读时InnoDB在READ COMMITTED和REPEATABLE READ隔离级别下处理SELECT语句的默认方式。一致性读不会在相关表上添加任何锁,因此其它 会话可以在同一时刻修改这些表中的记录。

假设你使用的是默认的REPEATABLE READ隔离级别。当你进行一致性读时(就是普通的SELECT语句),InnoDB给你的事务一个时间点,你的查询根据这个时间点来查询数据库。如果另一个事务删除了一行记录,并且提交时间在你的时间点之后,你就看不到这条记录被删除。插入和更新也是类似这样。

小贴士

数据库快照适用于同一个快照内的SELECT语句,不一定适用于DML语句。如果你插入或更新一些记录,然后提交事务,其它并发执行的REPEATABLE READ 隔离级别的事务中的DELETE或UPDATE语句可以影响到这些记录,即使在相关会话中无法查询到这些记录。举个例子,你可能会遇到与下面类似的情形:

select count(c) from t1 where c1 = 'xyz';
-- Returns 0: no rows match.
delete from t1 where c1 = 'xyz';
-- Deletes several rows recently committed by other transaction.
select count(c2) from t1 where c2 = 'abc';
-- Returns 0: no rows match.
update t1 set c2 = 'cba' where c2='abc';
-- Affects 10 rows: another txn just committed 10 rows with 'abc' values.
select count(c2) from t1 where c2 = 'cba';
-- Returns 10: this txn can now see the rows it just updated.

想要更新你的时间点,你可以提交事务,然后进行另一个SELECT或者START TRANSACTION WITH CONSISTENT SNAPSHOT语句。

这就是 multi-versioned concurrency control.

下面的例子中,只有在A和B都提交之后 session A才能看到session B的变更,这时时间点超过了B的提交。

SessionASessionB
SET autocommit=0;Set autocommit=0;
SELECT * FROM t; empty set
insert into t values(1,2);
select * from t; empty set
commit;
select * from t; empty set
commit;
select * from t; 1,2
如果你想要看到数据库的最新状态,可以使用READ COMMITTED隔离级别,或者是锁定读:
select * from t for share;

在READ COMMITTED隔离级别下,事务内的每个一致性读都建立并读取自己的快照。使用for share的话,就产生了锁定读:如果有其它事务修改 le数据,select会一直阻塞知道这个事务结束。

在一些DDL语句执行时,一致性读不起作用:

  • DROP TABLE。因为MySQL无法使用一个已经被删除的表的。
  • ALTER TABLE。因为此时会建立一个原始表的临时副本,并且在副本完成后删除原始表。如果你在事务内进行一致性读,新表内的记录是不可见的, 因为在事务快照建立时,这些记录不存在。这种场景下,事务会返回一个错误:ER_TABLE_DEF_CHANGED。“表定义已经改变,请重新执行事务”。

不同的SELECT语句使用的锁不都是一样的,如INSERT INTO … SELECT,UPDATE … (SELECT),以及CREATE TABLE … SELECT,这些SELECT 没有显式使用FOR UPDATE 或FOR SHARE:

  • 默认情况下,InnoDB在执行这些语句时使用更强类型的锁,和在READ COMMITTED隔离级别下相似,每个一致性读,即使在同一个事务内都使用自己的快照。
  • 如果想在这些场景下使用非锁定读,不想锁定读取到的行,可以将隔离级别修改为READ UNCOMMITTED或READ COMMITTED。