在串行化隔离中使用 SELECT FOR UPDATE

本页面介绍了如何在串行化隔离中使用 FOR UPDATE 子句。

FOR UPDATE 子句的锁定机制对于可重复读隔离串行化隔离是不同的。在串行化隔离级别下,当您使用 SELECT 查询扫描表时,添加 FOR UPDATE 子句会在行和列交叉的粒度级别(即单元格级别)启用独占锁。该锁在读写事务的整个生命周期内保持有效。在此期间,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 提示,而 FOR UPDATE 子句只能用于 SELECT 语句。

锁定语义

为了减少并发写入请求以及因死锁导致事务中止的成本,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 单元格,该范围与 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 = 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 子句时的查询语法。

最常见的用法是在顶级 SELECT 语句中。例如:

SELECT SingerId, SingerInfo
FROM Singers WHERE SingerID = 5
FOR UPDATE;

此示例演示了如何在 SELECT 语句中使用 FOR UPDATE 子句,以独占方式锁定 WHERE SingerID = 5SingerIdSingerInfo 单元格。

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

后续步骤