文档首页
MySQL 8.4 参考手册
相关文档 下载本手册
PDF (US Ltr) - 39.9Mb
PDF (A4) - 40.0Mb
手册页 (TGZ) - 258.5Kb
手册页 (Zip) - 365.5Kb
信息 (Gzip) - 4.0Mb
信息 (Zip) - 4.0Mb


17.7.2.4 锁定读取

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

  • SELECT ... FOR SHARE

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

    注意

    SELECT ... FOR SHARESELECT ... LOCK IN SHARE MODE 的替代方法,但 LOCK IN SHARE MODE 仍然为了向后兼容而可用。这些语句是等效的。但是,FOR SHARE 支持 OF table_nameNOWAITSKIP LOCKED 选项。请参阅 使用 NOWAIT 和 SKIP LOCKED 进行锁定读取并发

    SELECT ... FOR SHARE 需要 SELECT 权限。

    SELECT ... FOR SHARE 语句不会获取 MySQL 授予表上的读取锁。有关更多信息,请参阅 授予表并发

  • SELECT ... FOR UPDATE

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

    SELECT ... FOR UPDATE 需要 SELECT 权限,以及至少一种 DELETELOCK TABLESUPDATE 权限。

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

FOR SHAREFOR UPDATE 查询设置的所有锁在事务提交或回滚时都会被释放。

注意

只有在禁用 autocommit 时才有可能进行锁定读取(无论是通过使用 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 中?不能,因为其他会话可能在您的 SELECTINSERT 之间的瞬间删除父行,而您对此一无所知。

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

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

FOR SHARE 查询返回父 '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;

一个 SELECT ... FOR UPDATE 读取最新的可用数据,对它读取的每一行设置排他锁。因此,它设置与搜索的 SQL UPDATE 在行上设置的相同的锁。

前面的描述只是 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 事务必须等到阻塞事务释放行锁。这种行为可以防止事务更新或删除其他事务正在查询以更新的行。但是,如果您希望查询在请求的行被锁定后立即返回,或者如果从结果集中排除锁定行是可以接受的,则等待行锁释放是不必要的。

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

  • NOWAIT

    使用 NOWAIT 的锁定读取从不等待获取行锁。查询立即执行,如果请求的行被锁定,则返回错误。

  • SKIP LOCKED

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

    注意

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

NOWAITSKIP 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 |
+---+