如果您在同一事务中查询数据,然后插入或更新相关数据,普通的 SELECT
语句不能提供足够的保护。其他事务可以更新或删除您刚刚查询的行。InnoDB
支持两种类型的 锁定读取,它们提供了额外的安全性:
-
在读取的行上设置共享模式锁。其他会话可以读取这些行,但不能修改它们,直到您的事务提交。如果这些行被其他事务修改,而该事务尚未提交,您的查询将等待该事务结束,然后使用最新的值。
NoteSELECT ... 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
查询设置的锁都将在事务提交或回滚时释放。
锁定读取仅在禁用自动提交(通过开始事务 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
表并提交事务。任何尝试获取适用行的排他锁的事务都将等待,直到您完成,也就是说,直到所有表中的数据是一致的状态。
例如,考虑表 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
的锁定读从不等待获取行锁。查询立即执行,锁定的行从结果集中被删除。Note跳过锁定的行的查询返回不一致的数据视图。
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 |
+---+