使用 SELECT FOR UPDATE

本页介绍了如何在 Spanner 中使用 FOR UPDATE 子句。

使用 SELECT 查询扫描表时,请添加 FOR UPDATE 子句,以在行和列粒度级别(也称为单元格级别)启用排他锁。锁在读写事务的整个生命周期内保持有效。在此期间,FOR UPDATE 子句会阻止其他事务修改锁定的单元格,直到当前事务完成。如需了解详情,请参阅 GoogleSQLPostgreSQL FOR UPDATE 参考指南。

为什么要使用 FOR UPDATE 子句

在隔离级别较低的数据库中,可能需要使用 FOR UPDATE 子句来确保并发事务不会在读取数据和提交事务之间更新数据。由于 Spanner 始终强制执行序列化,因此只有在事务中访问的数据在提交时未过期的情况下,事务才会成功提交。因此,在 Spanner 中,无需使用 FOR UPDATE 子句来确保事务正确性。

但是,在写入争用较高的用例中,例如多个事务同时读取和写入相同的数据时,并发事务可能会导致中止次数增加。这是因为,当多个并发事务获取共享锁定,然后尝试升级到独占锁定时,这些事务会导致死锁。然后,Spanner 会只留下一个事务,而中止其他所有事务。如需了解详情,请参阅锁定

使用 FOR UPDATE 子句的事务会获取独占锁定并继续执行,而其他事务则会等待轮到其锁定。虽然 Spanner 可能会仍然限制吞吐量,因为有冲突的事务一次只能执行一个,但是由于 Spanner 仅在一个事务上取得进展,因此节省了中止和重试事务的时间。

因此,如果在同时写入请求场景中减少中止的事务数量很重要,那么您可以使用 FOR UPDATE 子句来减少总的中止次数并提高工作负载执行效率。

LOCK_SCANNED_RANGES 提示的比较

FOR UPDATE 子句的功能与 LOCK_SCANNED_RANGES=exclusive 提示类似。

主要有两个区别:

  • 如果您使用 LOCK_SCANNED_RANGES 提示,事务会对整个语句的扫描范围获取独占锁定。您无法对子查询获取排他性锁定。使用锁定提示可能会导致获取的锁定数量超出必要,并导致工作负载中出现锁定争用。以下示例展示了如何使用锁定提示:

    @{lock_scanned_ranges=exclusive}
    SELECT s.SingerId, s.FullName FROM Singers AS s
    JOIN (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000)
    AS a ON a.SingerId = s.SingerId;
    

    另一方面,您可以在子查询中使用 FOR UPDATE 子句,如以下示例所示:

    SELECT s.SingerId, s.FullName FROM Singers AS s
    JOIN (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000)
    FOR UPDATE AS a ON a.SingerId = s.SingerId;
    
  • 您可以在 DML 语句中使用 LOCK_SCANNED_RANGES 提示,但只能在 SELECT 语句中使用 FOR UPDATE 子句。

锁定语义

为了减少同时写入请求以及因死锁而中止事务的成本,Spanner 会尽可能在单元格级别锁定数据。使用 FOR UPDATE 子句时,Spanner 会锁定 SELECT 查询扫描的特定单元格。

在以下示例中,SingerId = 1AlbumId = 1 行中的 MarketingBudget 单元格在 Albums 表中处于排他性锁定状态,这会阻止并发事务修改该单元格,直到此事务提交或回滚。不过,并发事务仍可更新该行中的 AlbumTitle 单元格。

SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId = 1
FOR UPDATE;

并发事务可能会阻止读取锁定的数据

当一个事务对扫描范围获取了独占锁时,并发事务可能会阻止读取该数据。Spanner 会强制执行序列化,因此只有在事务生命周期内保证数据不会被其他事务更改的情况下,才能读取数据。尝试读取已锁定数据的并发事务可能必须等到持有锁的事务被提交或回滚。

在以下示例中,Transaction 1 会为 1 <= AlbumId < 5 锁定 MarketingBudget 单元格。

-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 5
FOR UPDATE;

尝试读取 AlbumId = 1MarketingBudgetTransaction 2 会被阻止,直到 Transaction 1 提交或回滚。

-- Transaction 2
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId = 1;

-- Blocked by Transaction 1

同样,尝试使用 FOR UPDATE 锁定扫描范围的事务会被锁定重叠扫描范围的并发事务阻止。

以下示例中的 Transaction 3 也被屏蔽,因为 Transaction 1 已锁定 3 <= AlbumId < 5MarketingBudget 单元格,而 3 <= AlbumId < 5 是与 Transaction 3 重叠的扫描范围。

-- Transaction 3
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 3 and AlbumId < 10
FOR UPDATE;

-- Blocked by Transaction 1

读取索引

如果锁定扫描范围的查询锁定了基表中的行,但并发事务从索引读取数据,则并发读取可能不会被阻止。

以下 Transaction 1 会为 SingerId = 1 锁定 SingerIdSingerInfo 单元格。

-- Transaction 1
SELECT SingerId, SingerInfo
FROM Singers
WHERE SingerId = 1
FOR UPDATE;

只读 Transaction 2 不会被 Transaction 1 中获取的锁屏蔽,因为它会查询索引表。

-- Transaction 2
SELECT SingerId FROM Singers;

并发事务不会阻止对已锁定数据执行 DML 操作

当一个事务通过独占锁提示获取一系列单元格的锁定时,并发事务可以尝试执行写入操作,而无需先读取锁定的单元格中的数据。事务会阻止提交,直到持有锁的事务提交或回滚。

以下 Transaction 1 会为 1 <= AlbumId < 5 锁定 MarketingBudget 单元格。

-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 5
FOR UPDATE;

如果 Transaction 2 尝试更新 Albums 表,则会被阻止执行此操作,直到 Transaction 1 提交或回滚。

-- Transaction 2
UPDATE Albums
SET MarketingBudget = 200000
WHERE SingerId = 1 and AlbumId = 1;

> Query OK, 1 rows affected

COMMIT;

-- Blocked by Transaction 1

扫描范围锁定后,现有行和间隔会被锁定

当一个事务对扫描范围获取了排他锁后,并发事务就无法在该范围内的间隙中插入数据。

以下 Transaction 1 会为 1 <= AlbumId < 10 锁定 MarketingBudget 单元格。

-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 10
FOR UPDATE;

如果 Transaction 2 尝试为尚不存在的 AlbumId = 9 插入行,则会被阻止执行此操作,直到 Transaction 1 提交或回滚。

-- Transaction 2
INSERT INTO Albums (SingerId, AlbumId, AlbumTitle, MarketingBudget)
VALUES (1, 9, "Hello hello!", 10000);

> Query OK, 1 rows affected

COMMIT;

-- Blocked by Transaction 1

锁定获取注意事项

所述的锁语义提供了一般性指导,但并不能保证在 Spanner 执行使用 FOR UPDATE 子句的事务时,锁的获取方式。Spanner 的查询优化机制也可能会影响获取哪些锁。该子句会阻止其他事务在当前事务完成之前修改锁定的单元格。

查询语义

本部分介绍了使用 FOR UPDATE 子句时的查询语义。

在 WITH 语句中使用

如果您在 WITH 语句的外层查询中指定 FOR UPDATEFOR UPDATE 子句不会为 WITH 语句获取锁。

在以下查询中,Singers 表不会获取任何锁定,因为锁定意图不会传播到通用表表达式 (CTE) 查询。

WITH s AS (SELECT SingerId, SingerInfo FROM Singers WHERE SingerID > 5)
SELECT * FROM s
FOR UPDATE;

如果在 CTE 查询中指定了 FOR UPDATE 子句,CTE 查询的扫描范围会获取锁定。

在以下示例中,SingerIdSingerInfo 单元格对应的行中 SingerId > 5 处于锁定状态。

WITH s AS
  (SELECT SingerId, SingerInfo FROM Singers WHERE SingerId > 5 FOR UPDATE)
SELECT * FROM s;

在子查询中使用

您可以在包含一个或多个子查询的外层级查询中使用 FOR UPDATE 子句。锁由顶级查询和子查询(表达式子查询除外)获取。

以下查询会锁定 SingerIdSingerInfo 单元格,其中 SingerId > 5.

(SELECT SingerId, SingerInfo FROM Singers WHERE SingerId > 5) AS t
FOR UPDATE;

以下查询不会锁定 Albums 表中的任何单元格,因为它位于表达式子查询中。表达式子查询返回的行对应的 SingerIdSingerInfo 单元格处于锁定状态。

SELECT SingerId, SingerInfo
FROM Singers
WHERE SingerId = (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000)
FOR UPDATE;

用于查询视图

您可以使用 FOR UPDATE 子句来查询视图,如以下示例所示:

CREATE VIEW SingerBio AS SELECT SingerId, FullName, SingerInfo FROM Singers;

SELECT * FROM SingerBio WHERE SingerId = 5 FOR UPDATE;

定义视图时,您无法使用 FOR UPDATE 子句。

不受支持的应用场景

不支持以下 FOR UPDATE 用例:

  • 作为在 Spanner 外部执行代码的互斥机制:请勿在 Spanner 中使用锁定来确保独享 Spanner 外部的资源。Spanner 可能会中止事务,例如,如果重试事务(无论是通过应用代码明确进行,还是通过客户端代码隐式进行,例如 Spanner JDBC 驱动程序),则只能保证在实际提交的尝试期间保持锁定。
  • LOCK_SCANNED_RANGES 提示结合使用:您不能在同一查询中同时使用 FOR UPDATE 子句和 LOCK_SCANNED_RANGES 提示,否则 Spanner 会返回错误。
  • 在全文搜索查询中:您无法在使用全文搜索索引的查询中使用 FOR UPDATE 子句。
  • 在只读事务中FOR UPDATE 子句仅在读写事务中执行的查询中有效。
  • 在 DDL 语句中:您无法在 DDL 语句中的查询中使用 FOR UPDATE 子句,因为这些查询会存储起来以便稍后执行。例如,在定义视图时,您无法使用 FOR UPDATE 子句。如果需要锁定,则可以在查询视图时指定 FOR UPDATE 子句。

后续步骤