如果查询数据然后在同一事务中插入或更新相关数据,则常规的 SELECT 语句无法提供足够的保护。其他事务可以更新或删除您刚刚查询的相同行。 InnoDB 支持两种类型的 锁定读取,它们提供额外的安全性
对读取的任何行设置共享模式锁。其他会话可以读取这些行,但不能修改它们,直到您的事务提交。如果这些行中的任何一行被另一个尚未提交的事务更改,您的查询将等待该事务结束,然后使用最新值。
注意SELECT ... FOR SHARE替换了SELECT ... LOCK IN SHARE MODE,但为了向后兼容,LOCK IN SHARE MODE仍然可用。这些语句是等效的。但是,FOR SHARE支持OF、table_nameNOWAIT和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 |
+---+