如果查询数据然后在同一事务中插入或更新相关数据,则常规的 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
事务必须等待阻止事务释放行锁。此行为可防止事务更新或删除其他事务查询以更新的行。但是,如果您希望查询在请求的行被锁定时立即返回,或者从结果集中排除锁定行是可以接受的,则不需要等待行锁释放。
为了避免等待其他事务释放行锁,可以在 SELECT ... FOR UPDATE
或 SELECT ... FOR SHARE
锁定读取语句中使用 NOWAIT
和 SKIP LOCKED
选项。
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 |
+---+