如果您查询数据,然后在同一事务中插入或更新相关数据,则常规的 SELECT
语句不会提供足够的保护。其他事务可以更新或删除您刚刚查询的相同行。 InnoDB
支持两种类型的 锁定读取,它们提供额外的安全性
对读取的任何行设置共享模式锁。其他会话可以读取这些行,但不能修改它们,直到您的事务提交。如果这些行中的任何一行被另一个尚未提交的事务更改,则您的查询将等待该事务结束,然后使用最新值。
注意SELECT ... FOR SHARE
是SELECT ... LOCK IN SHARE MODE
的替代方法,但LOCK IN SHARE MODE
仍然为了向后兼容而可用。这些语句是等效的。但是,FOR SHARE
支持OF
、table_name
NOWAIT
和SKIP LOCKED
选项。请参阅 使用 NOWAIT 和 SKIP LOCKED 进行锁定读取并发。SELECT ... FOR SHARE
需要SELECT
权限。SELECT ... FOR SHARE
语句不会获取 MySQL 授予表上的读取锁。有关更多信息,请参阅 授予表并发。对于搜索遇到的索引记录,锁定这些行以及任何关联的索引条目,就像您对这些行发出
UPDATE
语句一样。其他事务被阻止更新这些行、执行SELECT ... FOR SHARE
或在某些事务隔离级别中读取数据。一致性读取忽略对读取视图中存在的记录设置的任何锁。(旧版本的记录不能被锁定;它们是通过对记录的内存副本应用 回滚日志 来重建的。)SELECT ... FOR UPDATE
需要SELECT
权限,以及至少一种DELETE
、LOCK TABLES
或UPDATE
权限。
这些子句主要在处理树状结构或图状结构数据时有用,无论是在单个表中还是跨多个表拆分。您可以从一个地方遍历边或树分支到另一个地方,同时保留返回并更改这些 “指针” 值的权利。
由 FOR SHARE
和 FOR 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
中?不能,因为其他会话可能在您的 SELECT
和 INSERT
之间的瞬间删除父行,而您对此一无所知。
为了避免此潜在问题,请使用 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
语句仅检索标识符信息(特定于当前连接)。它不访问任何表。
如果一行被事务锁定,则请求同一锁定行的 SELECT ... FOR UPDATE
或 SELECT ... FOR SHARE
事务必须等到阻塞事务释放行锁。这种行为可以防止事务更新或删除其他事务正在查询以更新的行。但是,如果您希望查询在请求的行被锁定后立即返回,或者如果从结果集中排除锁定行是可以接受的,则等待行锁释放是不必要的。
为了避免等待其他事务释放行锁,可以使用 NOWAIT
和 SKIP LOCKED
选项与 SELECT ... FOR UPDATE
或 SELECT ... FOR SHARE
锁定读取语句。
NOWAIT
使用
NOWAIT
的锁定读取从不等待获取行锁。查询立即执行,如果请求的行被锁定,则返回错误。SKIP LOCKED
使用
SKIP LOCKED
的锁定读取从不等待获取行锁。查询立即执行,从结果集中删除锁定行。注意跳过锁定行的查询返回数据的不一致视图。因此,
SKIP LOCKED
不适合一般事务性工作。但是,它可用于在多个会话访问同一队列式表时避免锁争用。
NOWAIT
和 SKIP LOCKED
仅适用于行级锁。
使用 NOWAIT
或 SKIP LOCKED
的语句对于基于语句的复制是不安全的。
以下示例演示了 NOWAIT
和 SKIP 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 |
+---+