一 概念
一致读(consistent read),在《MySQL技术内幕 第二版》中称为一致性非锁定读(consistent nonlocking read),是指InnoDB使用多版本控制(multi versioning)向查询提供数据库在某个时间点的快照。
二 详细阐述
一致读查询能够看到在该时间点之前提交的事务所做的更改,而不会看到稍后或未提交的事务所做的更改。此规则的例外情况是,查询可以看到同一事务中早期语句所做的更改。这个例外导致了以下异常:如果更新表中的某些行,SELECT会看到更新行的最新版本,但也可能会看到任何行的旧版本。如果其他会话同时更新同一个表,则这个异常意味着你可能会看到该表处于数据库中从未存在过的状态。
如果事务隔离级别是可重复读(默认的隔离级别),同一个事务中的所有一致读读取的都是由事务中第一个一致读创建的快照。可以通过提交当前事务并在提交后发出新的查询,来为你的查询获取更新的快照。
在读已提交 隔离级别下,一个事务中的每个一致读都会设置并读取它自己的新快照。
一致读是InnoDB在读已提交 和 可重复读 隔离级别下处理SELECT语句的默认模式。一致读不会为它访问的表上设置任和锁,因此,其他会话可以在对表执行一致读取的同时自由修改这些表。
假设你正运行在默认的可重复读隔离级别下。当您发出一致的read(即普通的SELECT语句)时,InnoDB会给事务一个时间点,根据这个时间点,您的查询可以看到数据库。如果另一个事务删除一行并在分配了时间点后提交,则不会将该行视为已删除。插入和更新的处理方式类似。
注:
数据库状态的快照应用于事务中的SELECT语句,而不一定应用于DML语句。如果插入或修改某些行,然后提交该事务,则从另一个并发可重复读取事务发出的DELETE或UPDATE语句可能会影响那些刚刚提交的行,即使会话无法查询它们。如果某个事务确实更新或删除了其他事务提交的行,则这些更改对当前事务是可见的。例如,您可能会遇到以下情况:
代码语言:javascript复制SELECT COUNT(c1) 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查询 或 启动一致性快照的事务。这被称为多版本并发控制。
在下面的示例中,会话A仅在B提交了insert并且A也提交了insert时才看到B插入的行,因此时间点提前到B提交之后。
代码语言:javascript复制Session A Session B
SET autocommit=0; SET autocommit=0;
time
| SELECT * FROM t;
| empty set
| INSERT INTO t VALUES (1, 2);
|
v SELECT * FROM t;
empty set
COMMIT;
SELECT * FROM t;
empty set
COMMIT;
SELECT * FROM t;
---------------------
| 1 | 2 |
---------------------
如果你想要看到数据库的“最新”状态,可以使用读已提交隔离级别 或 锁定读:
代码语言:javascript复制SELECT * FROM t FOR SHARE;
三 读已提交
读已提交 隔离级别下,事务中的每个一致读设置并读取它自己的新快照。通过FOR SHARE,将会发生锁定读:SELECT语句被阻塞,直到包含最新行的事务结束
一致读在特定的DDL语句下不会生效:
1、一致读不适用于DROP TABLE语句,因为MySQL无法使用一个已经drop掉的表,InnoDB会销毁这张表。
2、一致读不适用于ALTER TABLE操作,ALTER TABLE会生成原始表的一个临时副本,并在临时副本建立后删除原始表。在事务中重新发出一致读取时,新表中的行不可见,因为在获取事务快照时,这些行不存在。在这种情况下,事务会返回错误:ER_TABLE_DEF_CHANGED,“表定义发生变化,请重试事务”。
SELECT语句读的类型各不相同,例如INSERT INTO...SELECT, UPDATE...(SELECT),以及CREATE TABLE ... SELECT,这些未指定FOR UPDATE 或 FOR SHARE:
1、默认情况下,InnoDB对这些语句使用更强的锁,SELECT部分的作用类似于读已提交,其中每个一致读(即使在同一事务中)都设置并读取自己的新快照。
2、要在这种情况下执行非锁定读取,请将事务的隔离级别设置为 读未提交 或读已提交,以避免对从所选表读取的行设置锁。
四 读已提交隔离级别下的一致读
如前面所说,READ COMMITTED事务隔离级别下,一致读总是读取行的最新版本,如果行被锁定,就读取该行版本的最新的快照。一个示例如下:
1、事务隔离级别设置/确认为读已提交
代码语言:javascript复制mysql> select @@tx_isolation;
----------------
| @@tx_isolation |
----------------
| READ-COMMITTED |
----------------
2、创建表,表t是一张示例表,建表语句:
代码语言:javascript复制CREATE TABLE `t` (
`i` int(11) NOT NULL,
PRIMARY KEY (`i`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
//插入1条数据
insert into t values(1);
3、开启会话A,begin开启事务:
代码语言:javascript复制mysql> begin;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from t where i=1;
---
| i |
---
| 1 |
---
1 row in set (0.00 sec)
mysql> select * from t where i=5;
Empty set (0.01 sec)
由于只有一条记录1,所以查询i=5时返回为空。
4、打开新的会话B,begin开启事务,执行更新动作,但先不提交:
代码语言:javascript复制mysql> begin
-> ;
Query OK, 0 rows affected (0.00 sec)
mysql> update t set i=5 where i=1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from t where i=1;
Empty set (0.00 sec)
mysql> select * from t where i=5;
---
| i |
---
| 5 |
---
1 row in set (0.00 sec)
5、回到会话A,查询i=5:
代码语言:javascript复制mysql> select * from t where i=5;
Empty set (0.01 sec)
由于B事务并未提交,而当前隔离级别为读已提交,所以查不到i=5的记录是符合预期的、
6、会话B提交事务:
代码语言:javascript复制mysql> commit;
Query OK, 0 rows affected (0.01 sec)
7、在回到会话A,查询i=5记录:
代码语言:javascript复制mysql> select * from t where i=5;
---
| i |
---
| 5 |
---
1 row in set (0.00 sec)
由此可见,对于READ COMMITTED事务隔离级别,从数据库理论来看,违背了事务ACID中的隔离性(I)。这也是我们在前面数据库事务模型文章中,对可重复读 隔离级别的一个实例证明。
我正在参与 腾讯云开发者社区数据库专题有奖征文。