Documentation Home

15.7.2.4 锁定读取

如果您查询数据,然后在同一事务中插入或更新相关数据,则常规SELECT 语句无法提供足够的保护。其他事务可以更新或删除您刚刚查询的相同行。 InnoDB支持两种提供额外安全性的 锁定读取

  • SELECT ... FOR SHARE

    在读取的任何行上设置共享模式锁。其他会话可以读取这些行,但在您的事务提交之前不能修改它们。如果这些行中的任何一行被另一个尚未提交的事务更改,您的查询将等待该事务结束,然后使用最新的值。

    笔记

    SELECT ... FOR SHARE是 的替代品SELECT ... LOCK IN SHARE MODE,但 LOCK IN SHARE MODE仍可用于向后兼容。这些陈述是等价的。但是,FOR SHARE支持、 和选项。请参阅 使用 NOWAIT 和 SKIP LOCKED 锁定读取并发OF table_nameNOWAITSKIP LOCKED

    在 MySQL 8.0.22 之前,SELECT ... FOR SHARE需要 权限以及、 或 权限SELECT中的至少一项。从 MySQL 8.0.22 开始,只 需要权限。 DELETELOCK TABLESUPDATESELECT

    从 MySQL 8.0.22 开始,SELECT ... FOR SHARE 语句不会在 MySQL 授权表上获取读锁。有关详细信息,请参阅 授予表并发性

  • SELECT ... FOR UPDATE

    对于搜索遇到的索引记录,锁定行和任何关联的索引条目,就像您为这些行发出 UPDATE语句一样。在某些事务隔离级别中,其他事务被阻止更新这些行、执行SELECT ... FOR SHARE或读取数据。一致读取忽略读取视图中存在的记录上设置的任何锁。(无法锁定旧版本的记录;通过 在记录的内存副本上 应用撤消日志来重建它们。)

    SELECT ... FOR UPDATE需要 特权以及、 或 特权SELECT中的至少一项。 DELETELOCK TABLESUPDATE

这些子句在处理树结构或图形结构数据时主要有用,无论是在单个表中还是拆分到多个表中。您从一个地方遍历边缘或树枝到另一个地方,同时保留返回并更改任何这些 指针值的权利。

当事务提交或回滚时, 由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,并确保该子行在表中有一个父行parent。您的应用程序代码可以确保整个操作序列中的引用完整性。

首先,使用一致性读取查询表 PARENT并验证父行是否存在。你能安全地将子行插入到表中 CHILD吗?SELECT不,因为其他一些会话可能会在您和您之间的那一刻删除父行 INSERT,而您却没有意识到。

为避免此潜在问题,请执行 SELECT使用FOR SHARE

SELECT * FROM parent WHERE NAME = 'Jones' FOR SHARE;

FOR SHARE查询返回 parent 后'Jones',您可以安全地将子记录添加到CHILD表中并提交事务。任何试图在 PARENT表中的适用行中获取排他锁的事务都会等待,直到您完成为止,也就是说,直到所有表中的数据处于一致状态。

再举一个例子,考虑一个表中的整数计数器字段CHILD_CODES,用于为添加到表中的每个子项分配一个唯一标识符 CHILD。不要使用一致性读取或共享模式读取来读取计数器的当前值,因为数据库的两个用户可能会看到相同的计数器值,如果两个事务尝试添加行,则会发生重复键错误与表相同的标识符CHILD

在这里,FOR SHARE这不是一个好的解决方案,因为如果两个用户同时读取计数器,则至少其中一个在尝试更新计数器时会陷入死锁。

要实现读取和递增计数器,首先使用 执行计数器的锁定读取FOR UPDATE,然后递增计数器。例如:

SELECT counter_field FROM child_codes FOR UPDATE;
UPDATE child_codes SET counter_field = counter_field + 1;

ASELECT ... FOR UPDATE读取最新的可用数据,在它读取的每一行上设置独占锁。因此,它设置了与搜索 SQLUPDATE 在行上设置的锁相同的锁。

前面的描述仅仅是如何 SELECT ... FOR UPDATE工作的一个例子。在 MySQL 中,生成唯一标识符的特定任务实际上可以仅使用对表的一次访问来完成:

UPDATE child_codes SET counter_field = LAST_INSERT_ID(counter_field + 1);
SELECT LAST_INSERT_ID();

SELECT语句仅检索标识符信息(特定于当前连接)。它不访问任何表。

使用 NOWAIT 和 SKIP LOCKED 锁定读取并发

如果一行被事务锁定,则请求同一锁定行的SELECT ... FOR UPDATESELECT ... FOR SHARE 事务必须等到阻塞事务释放行锁。此行为可防止事务更新或删除其他事务查询更新的行。但是,如果您希望查询在请求的行被锁定时立即返回,或者如果可以从结果集中排除锁定的行,则不必等待释放行锁。

为避免等待其他事务释放行锁, NOWAIT选项SKIP LOCKED 可以与SELECT ... FOR UPDATESELECT ... FOR SHARE 锁定读取语句一起使用。

  • NOWAIT

    使用NOWAIT永不等待获取行锁的锁定读取。查询立即执行,如果请求的行被锁定,则会失败并出现错误。

  • SKIP LOCKED

    使用SKIP LOCKED 永不等待获取行锁的锁定读取。查询立即执行,从结果集中删除锁定的行。

    笔记

    跳过锁定行的查询返回不一致的数据视图。SKIP LOCKED因此不适合一般事务性工作。但是,当多个会话访问同一个类似队列的表时,它可用于避免锁争用。

NOWAIT并且SKIP LOCKED 只适用于行级锁。

使用基于语句的复制NOWAITSKIP LOCKED对基于语句的复制不安全的语句。

下面的例子演示了NOWAITSKIP LOCKED。会话 1 启动一个事务,该事务对单个记录进行行锁定。会话 2 尝试使用该 NOWAIT选项对同一记录进行锁定读取。因为请求的行被会话 1 锁定,所以锁定读取会立即返回错误。在会话 3 中,锁定读取SKIP LOCKED返回请求的行,但会话 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 |
+---+