本頁面介紹不同的隔離等級,並說明這些等級在 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 1
會在 Transaction 2
提交後繼續執行。
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 1
不必中止,即使 Transaction 2
修改了 Transaction 1
讀取的資料也一樣。不過,Spanner 傳回的結果可能不是預期結果。
讀寫衝突和正確性
在先前的範例中,如果使用 SELECT
陳述式在 Transaction 1
中查詢的資料來制定後續行銷預算決策,可能會發生正確性問題。
舉例來說,假設總預算為 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 1
會在 Transaction 2
提交後繼續執行。
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
列。
後續步驟
瞭解如何使用可重複讀取隔離層級。
如要進一步瞭解 Spanner 可序列化和外部一致性,請參閱「TrueTime 與外部一致性」。