文档版本:8.0
来源:Consistent Nonlocking Reads 、Locking Reads
上一篇:事务隔离级别html
本篇主要介绍InnoDB的快照读与加锁读。
快照读严格来讲应该翻译为一致的无锁读(Consistent Nonlocking Reads),但国内通常都称做快照读。在以前的篇章中提到的普通读
、无锁读
和一致读
等等,实际上讲的都是快照读的概念。mysql
快照读,指的是InnoDB使用多版本机制,为一次查询提供数据库在特定时间点的快照。查询能够看到时间点以前已提交事务产生的变动,看不到未提交或时间点以后提交的。但有个特例,同一事物内快照生成以前产生的变动,也是能够看到的。这个特性会导致这样一种奇怪的情形:若是你更新了表的一些行,SELECT
语句能够看到你作出的变动,但也可能看到某些行的老版本。若是其它事务也更新了这个表的一些行,你所看到的表的状态可能从未在数据库中存在过。算法
若是事务的隔离级别是可重复读(默认级别),同一事务内的全部快照读都会读取事务内第一次快照读时创建的快照。经过提交当前事物和发起新的查询,能够刷新快照。sql
而在读已提交级别下,每次快照读都会创建最新快照。数据库
在读已提交和可重复读级别下,InnoDB会将SELECT
语句所有转换为快照读。快照读不会给它读取的表上任何锁,所以在快照读执行的同时,其它事务能够对表进行任意修改。
假设你在默认的可重复读级别下,当你发起一次快照读(也就是普通的SELECT
语句),InnoDB会给你的事务分配一个时间点:即记录查询看到数据库数据的时间点。若是其它事务在这个时间点以后删除了一行并提交,你的事务将不会感知到行被删除,插入和更新同理。安全
数据库快照机制会应用在事务中的SELECT
语句,但不会应用于DML语句。若是你插入或修改了某些行而后提交,同时另外一个可重复读事务发起的DELETE
或UPDATE
语句,能够影响到这些刚刚提交的行,即便不能查询到它们。若是一个事务对另外一个事务提交的行作更新或删除操做,这些原本不可见的变动将对前者可见。例如,你可能会遇到以下场景:并发
SELECT COUNT(c1) FROM t1 WHERE c1 = 'xyz'; -- 返回 0:没有匹配记录。 DELETE FROM t1 WHERE c1 = 'xyz'; -- 将删除其它事务最近提交的若干行。 SELECT COUNT(c2) FROM t1 WHERE c2 = 'abc'; -- 返回 0:没有匹配记录。 UPDATE t1 SET c2 = 'cba' WHERE c2 = 'abc'; -- 影响 10 行:另外一个事务刚刚提交了10条值为‘abc’的记录。 SELECT COUNT(c2) FROM t1 WHERE c2 = 'cba'; -- 返回 10: 这个事务如今能够看到它刚刚修改的行。
你能够经过提交事务并执行SELECT
或START TRANSACTION WITH CONSISTENT SNAPSHOT
语句来刷新快照时间点。翻译
上述的这些机制被称做多版本并发控制。日志
在下面的例子中,要想会话A看到会话B插入的行,必须让会话A和会话B都提交事务,从而使快照时间点刷新到B提交之后。code
Session A Session B SET autocommit=0; SET autocommit=0; 时间线 | SELECT * FROM t; | 空集 | INSERT INTO t VALUES (1, 2); | v SELECT * FROM t; 空集 COMMIT; SELECT * FROM t; 空集 COMMIT; SELECT * FROM t; --------------------- | 1 | 2 | ---------------------
若是你想保证看到数据库的最新状态,使用读已提交级别,亦或发起加锁读:
SELECT * FROM t FOR SHARE;
在读已提交级别下,同一事物内的每一次快照读都将设置本身的快照。而使用FOR SHARE语句,取而代之是产生一次加锁读:SELECT语句将阻塞直至事务得到到最新的行记录。
注意:FOR SHARE语句并不会刷新可重复读事务的快照时间点。
快照读对特定的DDL语句无效:
对于ALTER TABLE操做,MySQL提供三种算法:
- COPY:将在原表的拷贝中进行,随后表数据将从原表中逐行拷贝。期间不容许DML语句并行。
- INPLACE:不进行拷贝操做,但可能会原地重建表。在准备与执行阶段,ALTER操做可能会获取表的元数据独占锁。期间通常容许DML语句并行。
- INSTANT:只会修改数据字典中的元数据。在准备与执行阶段不会获取表的元数据独占锁,表数据也不会被影响,操做是瞬时的,容许DML语句并行。
当ALTER语句不支持INSTANT和INPLACE时,MySQL才会采起COPY算法进行拷贝-删除,例如更改字段顺序、改变字段类型或添加一个主键等等。关于触发COPY的各种条件详见:Online DDL Operations
对于SELECT语句的变种,如未明确FOR UPDATE
或FOR SHARE
的INSERT INTO ... SELECT
,UPDATE ... (SELECT)
和 CREATE TABLE ... SELECT
语句:
若是你在一个事务内先查询数据,再插入或更新与之相关联的数据,普通的SELECT语句不会给予足够的安全性保证。其余事务能够更新或删除你刚刚查询的行。InnoDB支持两种类型的加锁读来提供额外的安全性保证:
对查询到的行施加共享锁。其它事务能够读取这些行,但在你的事务提交前不能修改它们。若是其它事务事先修改了这些行而没有提交,你的查询将阻塞直至其它事务提交,随后获取到最新的值。
注意
SELECT ... FOR SHARE
是SELECT ... LOCK IN SHARE MODE
的替代语法,但LOCK IN SHARE MODE
仍保留下来做向后兼容。语句功能是一致的,但FOR SHARE
支持OF table_name
,NOWAIT
和and SKIP LOCKED
选项。详见Locking Read Concurrency with NOWAIT and SKIP LOCKED。
对于扫描到的索引值,锁定其所在行和任何关联的索引项,这一点与执行UPDATE
语句效果相同。其它事务若是对这些行进行更新、执行SELECT ... FOR SHARE
或特定隔离级别下读取,将阻塞。若是对快照视图中存在的行加锁,快照读会忽略这些锁。(老版本的行记录不能被锁定,由于它们是经过将行拷贝到内存中并执行回滚日志来重建的。)
SELECT ... FOR UPDATE
须要SELECT
权限,以及DELETE
,LOCK TABLES
或UPDATE
的至少一种权限。
不管在单表仍是多表中,上述两种语句在处理树形结构与图形结构的数据时很是有用。它们能在遍历图边界或穷举树分支的同时,保留了回退与改变“结点”值的权利。
FOR SHARE
和FOR UPDATE
查询所产生的锁将在事务提交或回滚后释放。
注意
加锁读只有在自动提交关闭的状况下才可用。(要么使用
START TRANSACTION
开启事务,要么设置autocommit
为0。)
声明在外部语句的加锁读不会锁定嵌套子查询中的行,除非加锁读也声明在子查询中。例如,下列语句不会锁定t2表中的行:
SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2) FOR UPDATE;
若要锁定t2表中的行,在子查询中声明加锁读:
SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2 FOR UPDATE) FOR UPDATE;
假设你想在child表插入一行,而且确保child行在parent表有一个父行。你的应用代码能够在这个操做序列中的确保参照完整性。
首先,使用快照读查询PARENT表来验证父行是否存在,然而你能安全地将子行插入CHILD表吗?不行,由于其它事务能够在你执行SELECT
和INSERT
操做之间删除该父行,而你没法感知到。
为了不这一可能发生的问题,发起带FOR SHARE
的SELECT
语句:
SELECT * FROM parent WHERE NAME = 'Jones' FOR SHARE;
在FOR SHARE
查询返回了父行‘Jones’后,你就能够安全地将子行插入到CHILD表并提交了。任未尝试获取PARENT表中可用行的独占锁的事务都会等待,直到你的事务结束,同时也表明每一个表中的数据都处于一致状态。
举另外一个例子,在CHILD_NODES表中设有一整形计数字段,用于给每一个插入到CHILD表的子行赋一个惟一标识。在这种状况下不要使用快照读或共享锁来读取当前计数器的值,由于两个数据库用户可能看到计数器的同一个值,并将在CHILD表插入相同标识值的行,因而会发生重复键错误。
在这种状况下,FOR SHARE
不是一个好的解决方案,若是两个事务同时读取到相同的计数值,至少会有一个事务在试图更新计数器时因死锁而终止。
如何实现对计数器的读取和迭代?先使用FOR UPDATE
对计数器发起加锁读,而后迭代计数器。例如:
SELECT counter_field FROM child_codes FOR UPDATE; UPDATE child_codes SET counter_field = counter_field + 1;
SELECT ... FOR UPDATE
语句读取最新的可用数据,为读取到的行设置独占锁。所以就能够在随后的UPDATE
中匹配到的行上设置相同的锁。
上面的案例只是为了说明SELECT ... FOR UPDATE
如何工做。在MySQL中,生成惟一标识的工做能够经过一条语句完成:
UPDATE child_codes SET counter_field = LAST_INSERT_ID(counter_field + 1); SELECT LAST_INSERT_ID();
其中SELECT
语句只是为了得到标识信息(与当前的数据库链接相关联),不会查询任何表。
若是事务锁住一行数据,其它事务对同一行发起SELECT ... FOR UPDATE
或SELECT ... FOR SHARE
查询将必须等待锁被释放。这一特性防止了那些查询出来并将被更新的行被其它事务更新或者删除。然而有时你想要查询行被上锁时语句当即返回,或者能够接受结果集中不包含被上锁的行时,就没有必要等待行锁被释放。
经过在SELECT ... FOR UPDATE
或SELECT ... FOR SHARE
中设置NOWAIT
与SKIP LOCKED
选项,能够避免没必要要的锁等待。
使用了SKIP LOCKED
的加锁读也不会等待获取行锁。查询当即执行,从结果集中剔除被上锁的行。
注意
跳过加锁行的查询将返回一个非一致性的数据视图。所以
SKIP LOCKED
不适合常规事务场景。但能够用于在多个事务访问队列类型的表时避免锁竞争。
NOWAIT
和SKIP LOCKED
只能用于行级锁。
在复制语句中使用NOWAIT
和SKIP LOCKED
是不安全的。
下面演示NOWAIT
和SKIP LOCKED
如何使用。会话1开启了一个事务并获取了一行行锁。会话2在同一行发起附带NOWAIT
选项的加锁读,由于请求行被会话1锁住,加锁读马上返回了错误。会话3发起附带SKIP LOCKED
的加锁读,则返回了不包含会话1锁住行的结果集。
# 会话 1: mysql> CREATE TABLE t (i INT, PRIMARY KEY (i)) ENGINE = InnoDB; mysql> INSERT INTO t (i) VALUES(1),(2),(3); mysql> START TRANSACTION; mysql> SELECT * FROM t WHERE i = 2 FOR UPDATE; +---+ | i | +---+ | 2 | +---+ # 会话 2: mysql> START TRANSACTION; mysql> SELECT * FROM t WHERE i = 2 FOR UPDATE NOWAIT; ERROR 3572 (HY000): Do not wait for lock. # 会话 3: mysql> START TRANSACTION; mysql> SELECT * FROM t FOR UPDATE SKIP LOCKED; +---+ | i | +---+ | 1 | | 3 | +---+