本页面介绍了不同的隔离级别,并说明了它们在 Spanner 中的运作方式。
隔离级别是一种数据库属性,用于定义哪些数据对并发事务可见。Spanner 支持 ANSI/ISO SQL 标准中定义的两个隔离级别:可序列化和可重复读。创建事务时,您需要为该事务选择最合适的隔离级别。所选的隔离级别可让各个事务优先考虑各种因素,例如延迟时间、中止率,以及应用是否容易受到数据异常的影响。最佳选择取决于工作负载的具体需求。
可序列化隔离
可序列化隔离是 Spanner 中的默认隔离级别。在可序列化隔离级别下,Spanner 为事务提供最严格的并发控制保证,称为外部一致性。Spanner 的行为就好像是所有事务都按顺序执行,即使 Spanner 实际在多个服务器(可能位于多个数据中心)上运行这些事务以获得比单服务器数据库更高的性能和可用性时也是如此。此外,如果一个事务在另一个事务开始提交之前完成,Spanner 会保证客户端始终按顺序看到事务的结果。直观地说,Spanner 类似于单机数据库。
但缺点是,如果工作负载的读写争用情况严重(即许多事务读取的数据正在被另一个事务更新),Spanner 可能会中止事务,这是由可序列化事务的基本性质决定的。不过,对于运营数据库来说,这是一个不错的默认值。这有助于您避免通常只在高并发情况下才会出现的棘手的时间问题。这些问题很难重现和排查。因此,可序列化隔离可提供最强大的保护,防范数据异常。如果需要重试事务,则事务重试可能会导致延迟时间增加。
可重复读隔离
在 Spanner 中,可重复读隔离是使用一种通常称为快照隔离的技术实现的。Spanner 中的可重复读隔离可确保事务中的所有读取操作都能看到事务开始时数据库的一致(或强一致性)快照。它还保证,只有在没有冲突的情况下,对同一数据的并发写入才会成功。此方法适用于读写冲突严重的情况,即许多事务读取的数据可能正在被其他事务修改。通过使用固定快照,可重复读避免了限制性更强的可序列化隔离级别带来的性能影响。读取操作无需获取锁即可执行,也不会阻塞并发写入操作,从而减少了因潜在的序列化冲突而可能需要重试的已中止事务。如果您的客户端已在读写事务中运行所有内容,并且难以重新设计和使用只读事务,那么您可以使用可重复读隔离来缩短工作负载的延迟时间。
与可序列化隔离不同,如果您的应用依赖于数据库架构未强制执行的特定数据关系或约束条件,可重复读可能会导致数据异常,尤其是在操作顺序很重要时。在这种情况下,即使数据库架构约束条件仍然满足,事务也可能会读取数据、根据这些数据做出决策,然后写入违反这些应用特定约束条件的更改。这是因为可重复读隔离允许并发事务在没有严格序列化的情况下继续进行。一种潜在的异常情况称为写倾斜,它是由一种特定的并发更新引起的,其中每个更新都被独立接受,但它们的组合效应违反了应用数据完整性。例如,假设有一个医院系统,其中至少需要有一位医生随时待命,并且医生可以申请在某个班次中不待命。在可重复读隔离级别下,如果 Richards 医生和 Smith 医生都安排在同一班次值班,并同时尝试请求取消值班,则每个请求都会并行成功。这是因为这两个事务都读取到在事务开始时至少还有一位医生已安排好值班,如果这两个事务都成功,就会导致数据异常。另一方面,使用可序列化隔离可防止这些事务违反约束,因为可序列化事务会检测到潜在的数据异常并中止事务。从而通过接受更高的中止率来确保应用的一致性。
在前面的示例中,您可以在可重复读隔离中使用 SELECT FOR UPDATE
子句。SELECT…FOR UPDATE
子句用于验证在所选快照中读取的数据在提交时是否保持不变。同样,在内部读取数据以确保写入完整性的 DML 语句和更改也会验证数据在提交时是否保持不变。
如需了解详情,请参阅使用可重复读隔离。
用例示例
以下示例展示了使用可重复读隔离来消除锁定开销的好处。Transaction 1
和 Transaction 2
均在可重复读隔离级别下运行。
Transaction 1
在 SELECT
语句运行时建立快照时间戳。
GoogleSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
/*-----------+------------------*
| AlbumId | MarketingBudget |
+------------+------------------+
| 1 | 50000 |
| 2 | 100000 |
| 3 | 70000 |
| 4 | 80000 |
*------------+------------------*/
PostgreSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
/*-----------+------------------*
| albumid | marketingbudget |
+------------+------------------+
| 1 | 50000 |
| 2 | 100000 |
| 3 | 70000 |
| 4 | 80000 |
*------------+------------------*/
然后,Transaction 2
在 Transaction 1
开始后但在提交之前建立快照时间戳。由于 Transaction 1
尚未更新数据,因此 Transaction 2
中的 SELECT
查询读取的数据与 Transaction 1
相同。
GoogleSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 > T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);
COMMIT;
PostgreSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 > T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);
COMMIT;
在 Transaction 2
提交后,Transaction 1
继续。
GoogleSQL
-- Transaction 1 continues
SELECT SUM(MarketingBudget) as UsedBudget
FROM Albums
WHERE SingerId = 1;
/*-----------*
| UsedBudget |
+------------+
| 300000 |
*------------*/
PostgreSQL
-- Transaction 1 continues
SELECT SUM(marketingbudget) AS usedbudget
FROM albums
WHERE singerid = 1;
/*-----------*
| usedbudget |
+------------+
| 300000 |
*------------*/
Spanner 返回的 UsedBudget
值是 Transaction 1
读取的预算之和。此总数仅反映 T1
快照中存在的数据。它不包括 Transaction 2
添加的预算,因为 Transaction 2
在 Transaction 1
建立快照 T1
后提交了更改。使用可重复读意味着,即使 Transaction 2
修改了 Transaction 1
读取的数据,Transaction 1
也不必中止。不过,Spanner 返回的结果可能符合预期,也可能不符合预期。
读写冲突和正确性
在前面的示例中,如果 Transaction 1
中 SELECT
语句查询的数据用于制定后续的营销预算决策,则可能会出现正确性问题。
例如,假设总预算为 400,000
。根据 Transaction 1
中 SELECT
语句的结果,我们可能会认为预算中还剩下 100,000
,并决定将其全部分配给 AlbumId = 4
。
GoogleSQL
-- Transaction 1 continues..
UPDATE Albums
SET MarketingBudget = MarketingBudget + 100000
WHERE SingerId = 1 AND AlbumId = 4;
COMMIT;
PostgreSQL
-- Transaction 1 continues..
UPDATE albums
SET marketingbudget = marketingbudget + 100000
WHERE singerid = 1 AND albumid = 4;
COMMIT;
Transaction 1
成功提交,即使 Transaction 2
已将剩余预算 100,000
的 50,000
分配给新专辑 AlbumId = 5
。
您可以使用 SELECT...FOR UPDATE
语法来验证事务的某些读取操作在事务的整个生命周期内是否保持不变,从而保证事务的正确性。在以下使用 SELECT...FOR UPDATE
的示例中,Transaction 1
在提交时中止。
GoogleSQL
-- Transaction 1 continues..
SELECT SUM(MarketingBudget) AS TotalBudget
FROM Albums
WHERE SingerId = 1
FOR UPDATE;
/*-----------*
| TotalBudget |
+------------+
| 300000 |
*------------*/
COMMIT;
PostgreSQL
-- Transaction 1 continues..
SELECT SUM(marketingbudget) AS totalbudget
FROM albums
WHERE singerid = 1
FOR UPDATE;
/*-------------*
| totalbudget |
+-------------+
| 300000 |
*-------------*/
COMMIT;
如需了解详情,请参阅在可重复读隔离中使用 SELECT FOR UPDATE。
写-写冲突和正确性
使用可重复读隔离级别时,对同一数据的并发写入只有在没有冲突的情况下才会成功。
在以下示例中,Transaction 1
在第一个 SELECT
语句处建立快照时间戳。
GoogleSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
PostgreSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
以下 Transaction 2
读取的数据与 Transaction 1
相同,并插入一个新项。Transaction 2
成功提交,无需等待或中止。
GoogleSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 (> T1)
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);
COMMIT;
PostgreSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 (> T1)
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);
COMMIT;
在 Transaction 2
提交后,Transaction 1
继续。
GoogleSQL
-- Transaction 1 continues
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;
PostgreSQL
-- Transaction 1 continues
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;
Transaction 1
中止,因为 Transaction 2
已将插入提交到 AlbumId = 5
行。
后续步骤
了解如何使用可重复读隔离级别。
了解如何在可重复读隔离中使用 SELECT FOR UPDATE。
了解如何在可序列化隔离中使用 SELECT FOR UPDATE。
如需详细了解 Spanner 的可序列化和外部一致性,请参阅 TrueTime 和外部一致性。