如果你在同一个事务中先查询数据,再插入或更新相关数据,常规的SELECT语句无法提供足够的保护。其它事务可以更新或删除你刚刚查询的行。 为了提供额外的安全性,InnoDB支持两种类型的锁定读:
- SELECT … FOR SHARE 在读取的记录上添加共享锁。如果其中某些记录已被其它未提交的事务修改,那你的查询则需要等待,直到对方结束。
小贴士:
SELECT ... FOR SHARE 是用来替代 SELECT ... LOCK IN SHARE MODE的,但是LOCK IN SHARE MODE 仍然可用。这两个语句是等价的。但是FOR SHARE 支持 OF table_name,NOWAIT选项。
在MySQL8.0.22之前,SELECT … FOR SHARE要求SELECT权限和DELETE,LOCK TABLES,或UPDATE中至少一项权限。从MySQL8.0.22开始, 只需要SELECT权限。
从MySQL8.0.22开始,SELECT … FOR SHARE语句不再获取级读锁。
- SELECT … FOR UPDATE 和UPDATE语句一样,在查找索引记录的过程中,锁定相关的行和相关的索引条目。如果其它事务要更新这些记录,或使用SELECT … FOR SHARE, 或在特定隔离级别下读取数据,将发生阻塞。一致性读会忽略视图中相关记录上的锁。(旧版本的记录不能被锁定;它们通过记录的内存拷贝和undo 日志被重建。) SELECT … FOR UPDATE要求SELECT权限和DELETE,LOCK TABLES,或UPDATE中至少一项权限。
这些子句在处理树状结构或图形结构数据时非常有用。可以在单表中使用,也可以在分割多张表。可以遍历图形边缘或树的分支,同时保留更改这些" 指针"的权利。 FOR SHARE和FOR UPDATE添加的锁将在事务结束时被释放。
小贴士
只有自动提交被禁用时,锁定读才可以使用。
外部语句中的锁定读不会锁定子查询中的记录。在下面的例子中,不会锁定t2中的记录。
select * from where c1 = (select c1 from t2) for update;
要想锁定t2中的记录,可以在子查询中的记录添加锁:
select * from t1 where c1 = (select c1 from t2 for update) for update;
锁定读例子 假设你要向child表中插入一条记录,并要确保parent表中存在一条上级记录。你的应用代码可以保证这一系列操作的完整性。
实现,使用一致性读查询parentbiao ,确保相关记录已存在。你能安全地向child表插入吗?不能,因为其它会话可以在你的SELECT和INSERT 语句之间删除parent表中的相关记录,你无法察觉。
为了避免这个潜藏的问题,可以在SELECT时加上FOR SHARE:
select * from parent where name='Jones' for share;
在FOR SHARE查询返回 ‘Jones’对应的记录后,你可以安全地向child表插入记录,然后提交事务。其它事务想要获取parent表中相关记录上的 排它锁的话,就要等待你的事务结束,那时,所有表中的数据会处于一种一致性状态。
再举一个例子。假设有一张表child_codes,中有一个整型计数字段,用来记录child表中的记录数。不要使用一致性读和共享锁定读来查询当前的 计数值,因为数据库的其它用户可以看到相同的计数值,而且如果两个事务向child表中插入具有相同标识的记录,就会发生duplicate-key错误。
这种情形下,FOR SHARE不是一个好的选择。因为如果两个用户同时读取计数值,至少其中一个会在更新计数值的时候发生死锁。
要读取并更新计数值,首先使用FOR UPDATE进行锁定读,然后修改计数值。例如:
select counter_field from child_codes for update;
update child_codes set counter_field = counter_field + 1;
SELECT … FOR UPDATE获取最新的数据,在读取的每一条记录上添加排它锁。因此,它和普通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 UPDATE和SELECT … FOR SHARE加锁时,将会被阻塞, 直到持有锁的事务释放锁。这种行为阻止了其它事务更新或删除被锁定的记录。但是,如果你想让要求的记录被锁定时立即返回,或 在结果集中排出被锁定的行,那等待就没有必要了。 如果不想等待其它事务释放锁,可以在SELECT … FOR SHARE或SELECT … FOR UPDATE后添加NOWAIT和SKIP LOCKED选项。
NOWAIT 有NOWAIT选项的锁定读,不会等待。如果要求的记录已被锁定,那么它会立即返回。
SKIP LOCKED 使用SKIP LOCKED的锁定读,不会等待。查询会立即返回,结果集中不会包含已被锁定的记录。
小贴士
查询时跳过已锁定的记录,会返回非一致性的数据。所以 SKIP LOCKED不适用于通用事务性工作。但是,在多个会话查询同一张时,它可以用来避免锁争夺。
NOWAIT和SKIP LOCKED只能应用于行级锁。 使用NOWAIT和SKIP LOCKED对基于语句的复制是不安全的。