17.7.2.4 锁定读取
如果您查询数据,然后在同一事务中插入或更新相关数据,常规SELECT
语句不足以提供保护。其他事务可以更新或删除您刚刚查询的行。InnoDB
支持两个类型的锁定读取,提供额外的安全性:
-
在读取任何行时设置共享模式锁。其他会话可以读取这些行,但直到您的事务提交它们不能修改。如果这些行被另一个尚未提交的事务所更改,您的查询将等待该事务结束,然后使用最新值。
NoteSELECT ... FOR SHARE
是SELECT ... LOCK IN SHARE MODE
的替代品,但LOCK IN SHARE MODE
仍然可用于向后兼容性。两个语句是等效的。但是,FOR SHARE
支持OF
、table_name
NOWAIT
和SKIP LOCKED
选项。请参阅Locking Read Concurrency with NOWAIT and SKIP LOCKED。SELECT ... FOR SHARE
需要SELECT
权限。SELECT ... FOR SHARE
语句不在 MySQL 授权表上获取读锁。更多信息,请见授权表并发性。 -
对于索引记录,搜索将锁定行和相关的索引条目,就像对这些行执行了一个
UPDATE
语句一样。其他事务将被阻塞,以便不能更新这些行,也不能执行SELECT ... FOR SHARE
语句,或者在某些事务隔离级别下读取数据。consistent reads 将忽略对读视图中记录的锁定。(老版本的记录无法锁定;它们将通过在内存副本上应用undo日志来重建。)SELECT ... FOR UPDATE
需要SELECT
权限,以及至少一个DELETE
、LOCK TABLES
或UPDATE
权限。
这些子句主要有助于处理树结构或图结构数据,可能分布在单个表中或跨多个表。您可以从一个地方到另一个地方遍历边缘或树枝,同时保留更改这些“指针”值的权利。
所有由FOR SHARE
和FOR UPDATE
查询设置的锁定都将在事务提交或回滚时释放。
只有在自动提交被禁用(通过开始事务或设置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
中,并确保该子行在表parent
中存在。您的应用程序代码可以确保引用完整性整个操作序列。
首先,使用一致读查询表PARENT
并验证父行是否存在。您可以安全地将子行插入到表CHILD
中吗?不,因为其他会话可能在您的SELECT
和INSERT
之间删除父行,而您不知道。
为了避免这个潜在问题,使用FOR SHARE
来执行SELECT
:
SELECT * FROM parent WHERE NAME = 'Jones' FOR SHARE;
在执行了FOR SHARE
查询后,您可以安全地将子记录添加到CHILD
表中,并提交事务。任何尝试在PARENT
表中的适用行上获取排他锁的事务都将等待您完成,即直到所有表中的数据处于一致状态。
例如,考虑一个用于为每个子记录分配唯一标识符的整数计数字段在表CHILD_ CODES
中。不要使用一致读或共享模式读来读取计数器的当前值,因为两个数据库用户可能看到相同的计数器值,如果两个事务尝试将具有相同标识符的行添加到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
读取最新可用的数据,设置每个行上的排他锁。因此,它设置了与搜索SQLUPDATE
将设置的相同锁。
以下描述仅是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
事务必须等待阻塞事务释放行锁。这行为防止了事务更新或删除被其他事务查询更新的行。然而,如果你想要查询在请求行锁时立即返回,或者排除锁定的行从结果集中,则不需要等待行锁释放。
避免等待其他事务释放行锁,可以使用NOWAIT
和SKIP LOCKED
选项与SELECT ... FOR UPDATE
或SELECT ... FOR SHARE
锁定读语句。
-
NOWAIT
使用
NOWAIT
的锁定读从不等待获取行锁。查询立即执行,如果请求行被锁定,则失败并返回错误。 -
SKIP LOCKED
使用
SKIP LOCKED
的锁定读操作从不等待获取行锁。查询立即执行,删除结果集中被锁定的行。Note跳过锁定的查询返回数据的不一致视图。
SKIP LOCKED
因此不适合一般事务工作。但是,它可能用于避免多个会话访问同一个队列-like 表时的锁定竞争。
NOWAIT
和SKIP LOCKED
仅适用于行级锁定。
使用NOWAIT
或SKIP LOCKED
的语句不安全用于语句基于的复制。
以下示例演示了NOWAIT
和SKIP LOCKED
。会话1开始一个事务,锁定单个记录。会话2尝试使用NOWAIT
选项对同一记录进行锁定读操作,因为请求的行被会话1锁定,锁定读操作立即返回错误。在会话3中,使用SKIP LOCKED
的锁定读操作返回请求的行 except for the row that is locked by Session 1.
# Session 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 |
+---+
# Session 2:
mysql> START TRANSACTION;
mysql> SELECT * FROM t WHERE i = 2 FOR UPDATE NOWAIT;
ERROR 3572 (HY000): Do not wait for lock.
# Session 3:
mysql> START TRANSACTION;
mysql> SELECT * FROM t FOR UPDATE SKIP LOCKED;
+---+
| i |
+---+
| 1 |
| 3 |
+---+