4,一致性的非锁定读操作
4.1,CNR原理解析
一致性的非锁定行读(consistent nonlocking read,简称CNR)是指InnoDB存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中运行的数据。如果读取的行正在执行delete、update操作,这时读取操作不会因此而会等待行上锁的释放,相反,InnoDB存储引擎会去读取行的一个快照数据,如下图所示:
MySQL InnoDB存储引擎中的锁" src="/uploads/allimg/200603/0425241104_0.png" />
非锁定读,是因为不需要等待访问的行上X锁的释放,快照数据是指该行之前版本的数据,该实现是通过Undo段来实现,而Undo用来在事务中回滚数据,因此快照本身是没有额外的开销,此外读取快照是不需要上锁的,因为没有必要对历史的数据进行修改。
非锁定读大大提高了数据读取的并发性,在InnoDB存储引擎默认设置下,这是默认的读取方式,既读取不会占用和等待表上的锁。但是不同事务隔离级别下,读取的方式不同,不是每一个事务隔离级别下的都是一致性读。同样,即使都是使用一致性读,但是对于快照数据的定义也不相同。
快照数据其实就是当前数据之前的历史版本,可能有多个版本。如上图所示,一个行可能不止有一个快照数据。我们称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,MVCC)。
在Read Committed和Repeatable Read模式下,innodb存储引擎使用默认的非锁定一致读。在Read Committed隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据;而在Repeatable Read隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。
4.2,CNR实例
开启2个Session A和B。
Session A:
MySQL> begin;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from t1 where a=1;
+---+----+----+
| a | b | c |
+---+----+----+
| 1 | c2 | c2 |
+---+----+----+
1 row in set (0.00 sec)
mysql>
Session A中事务已经开始,读取了a=1的数据,但是还没有结束事务,这时我们再开启一个Session B,以此模拟并发的情况,然后对Session B做如下操作:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update t1 set a=111 where a=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql>
Session 中将a=1的行修改为a=111,但是事务同样没有提交,这样a=1的行其实加了一个X锁。这时如果再在Session A中读取a=1的数据,根据innodb存储引擎的特性,在Read Committed和Repeatable Read事务隔离级别下,会使用非锁定的一致性读。回到Session A,节着上次未提交的事务,执行select * from t1 where a=1;的操作,显示的数据应该都是原来的数据:
mysql> select * from t1 where a=1;
+---+----+----+
| a | b | c |
+---+----+----+
| 1 | c2 | c2 |
+---+----+----+
1 row in set (0.00 sec)
mysql>
因为当前a=1的数据被修改了1次,所以只有一个版本的数据,接着我们在Session B中commit上次的事务。如:
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql>
Session B提交事务后,这时再在Session A中运行select * from t1 where a=1;的sql语句,在READ-COMMITTED和REPEATABLE-READ事务隔离级别下,得到的结果就会不一样,对于READ-COMMITTED模事务隔离级别,它总是读取行的最新版本,如果行被锁定了,则读取该行的最新一个快照(fresh snapshot)。因此在这个例子中,因为Session B已经commit了事务,所以在READ-COMMITTED事务隔离级别下会得到如下结果,查询a=1就是为null记录,因为a=1的已经被commit成了a=111,但是如果查询a=111的记录则会被查到,如下所示:
mysql> show variables like 'tx_isolation';
+---------------+----------------+
| Variable_name | Value |
+---------------+----------------+
| tx_isolation | READ-COMMITTED |
+---------------+----------------+
1 row in set (0.00 sec)
mysql> select * from t1 where a=1;
Empty set (0.00 sec)
mysql> select * from t1 where a=111;
+-----+----+----+
| a | b | c |
+-----+----+----+
| 111 | c2 | c2 |
+-----+----+----+
1 row in set (0.01 sec)
mysql>
但是如果在REPEATABLE-READ事务隔离级别下,总是读取事务开始时的数据,所以得到的结果截然不同,如下所示:
mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)
mysql> select * from t1 where a=1;
+---+----+----+
| a | b | c |
+---+----+----+
| 1 | c0 | c2 |
+---+----+----+
1 row in set (0.00 sec)
mysql> select * from t1 where a=111;
Empty set (0.00 sec)
mysql>
对于READ-COMMITTED的事务隔离级别而言,从数据库理论的角度来看,其实违反了事务ACID的I的特性,既是隔离性,整理成时序表,如下图所示。
Time
Session A
Session B
| time 1
Begin;
Select * from t1 where a=1;有记录
| time 2
Begin;
Update t1 set a=111 where a=1;
| time 3
Select * from t1 where a=1;有记录
| time 4
Commit;
| time 5
Select * from t1 where a=1; 无记录
V time 6
Commit;
如果按照ACID原理中的I原理隔离性,在整个会话中Session A中,Select * from t1 where a=1;应该查询出来的数据保持一直,但是在time 5那一刻 Session A未结束的时候,查询出来的结果已经变化了和time 1、time 3已经不一致了,不满足ACID的隔离性。