使用 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 查询扫描的特定单元格。

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

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

并发事务在读取已锁定的数据时可能会阻塞

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

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

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

Transaction 2 会尝试读取 AlbumId = 1MarketingBudget,并会被阻塞,直到 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 < 5Transaction 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 = 1SingerIdSingerInfo 单元格。

-- 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 < 5MarketingBudget 单元格。

-- 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 < 10MarketingBudget 单元格。

-- 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 UPDATE 时,FOR UPDATE 子句不会为 WITH 语句获取锁。

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

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

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

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

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

在子查询中使用

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

以下查询会锁定 SingerId > 5.SingerIdSingerInfo

(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 子句。

后续步骤