隔離等級總覽

本頁面將介紹不同的隔離等級,並說明這些等級在 Spanner 中的運作方式。

隔離等級是資料庫屬性,用於定義並行交易可查看的資料。Spanner 支援 ANSI/ISO SQL 標準中定義的兩種隔離層級:可序列化和可重複讀取。建立交易時,您需要為交易選擇最合適的隔離等級。所選隔離層級可讓個別交易優先考量各種因素,例如延遲時間、中止率,以及應用程式是否容易受到資料異常的影響。最適合的選擇取決於工作負載的具體需求。

可序列化隔離

可序列化隔離是 Spanner 的預設隔離等級。在可序列化隔離下,Spanner 提供最嚴格的交易並行控制保證,稱為外部一致性。Spanner 的行為有如所有交易都是依序執行,即使 Spanner 實際上是在多個伺服器 (或可能是多個資料中心) 執行交易,以獲得比單一伺服器資料庫更高的效能與可用性。此外,若一筆交易在另一筆交易開始修訂前完成,Spanner 保證用戶端一律會依序看到交易結果。直覺上,Spanner 類似於單一機器資料庫。

但如果工作負載的讀寫爭用率偏高,也就是許多交易讀取其他交易正在更新的資料,Spanner 可能會中止交易,這是可序列化交易的本質所致。不過,這對於作業資料庫來說是不錯的預設值。這有助於避免通常只會在高並行情況下發生的棘手時間問題。這類問題難以重現和排解。因此,可序列化隔離等級可提供最強大的防護機制,防範資料異常。如果需要重試交易,延遲時間可能會因重試交易而增加。

可重複讀取隔離

在 Spanner 中,可重複讀取隔離是透過一般稱為快照隔離的技術實作。Spanner 的可重複讀取隔離功能可確保交易中的所有讀取作業,都會看到交易開始時資料庫的一致或強式快照。此外,如果沒有衝突,系統也會確保對相同資料的並行寫入作業只會成功。在讀取/寫入衝突頻繁的情境中,許多交易會讀取其他交易可能修改的資料,此時這個方法就相當實用。使用固定快照時,可重複讀取作業可避免更嚴格的可序列化隔離層級對效能造成的影響。

由於預設採用樂觀並行,讀取作業執行時不會取得鎖定,也不會封鎖並行寫入作業,因此因潛在序列化衝突而需要重試的交易較少。使用悲觀並行時,讀取作業會使用快照,但專屬鎖定會套用至從 FOR UPDATE 查詢或 lock_scanned_ranges=exclusive 提示讀取的資料,以及使用 DML 查詢寫入的資料。

如果是從其他資料庫遷移的工作負載,建議您將應用程式設定為在 Spanner 中使用可重複讀取隔離層級。可重複讀取交易語意 (特別是讀取鎖定) 符合大多數其他資料庫 (例如 MySQL 和 PostgreSQL) 的預設隔離層級。這樣一來,您就不必重新設計應用程式,即可搭配 Spanner 的預設可序列化隔離層級使用。

與可序列化隔離不同,如果應用程式依賴資料庫結構定義未強制執行的特定資料關係或限制,重複讀取可能會導致資料異常狀況,尤其是在作業順序很重要時。在這種情況下,交易可能會讀取資料、根據該資料做出決策,然後寫入違反這些應用程式專屬限制的變更,即使資料庫結構定義限制仍符合規定也一樣。這是因為可重複讀取隔離允許並行交易繼續進行,而不需嚴格序列化。其中一種潛在異常狀況稱為「寫入偏斜」,這是由特定類型的並行更新所造成,每個更新都會獨立接受,但合併效果會違反應用程式資料完整性。舉例來說,假設某個醫院系統需要至少一名醫生隨時待命,而醫生可以要求在某個班別免除待命義務。在可重複讀取隔離層級下,如果 Richards 醫生和 Smith 醫生都排定在同一班次待命,並同時要求取消待命,則每項要求都會平行成功。這是因為兩筆交易都讀取到至少有一位醫生已排定在交易開始時待命,如果交易成功,就會導致資料異常。另一方面,使用可序列化隔離可防止這些交易違反限制,因為可序列化交易會偵測潛在的資料異常,並中止交易。因此,接受較高的中止率,確保應用程式一致性。

在上一個範例中,您可以在可重複讀取隔離中使用 SELECT FOR UPDATE 子句。SELECT ... FOR UPDATE 子句會驗證在所選快照中讀取的資料,在提交時是否維持不變。同樣地,內部讀取資料以確保寫入作業完整性的 DML 陳述式和變異,也會在修訂時驗證資料是否維持不變。此外,透過悲觀並行,SELECT ... FOR UPDATE讀取的資料和 DML 陳述式寫入的資料會取得專屬鎖定,防止任何後續交易在目前交易修訂前,修訂衝突的內容。

如果工作負載是從使用 FOR UPDATE 查詢的其他資料庫遷移,建議您在 Spanner 中,將應用程式設定為使用悲觀並行控制的可重複讀取隔離層級。應用程式會繼續取得 SELECT ... FOR UPDATE 所讀取資料的鎖定,這是其他資料庫的預設行為。

詳情請參閱「使用可重複讀取隔離層級」。

用途範例

以下範例說明使用可重複讀取隔離層級的好處,可消除鎖定負擔。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」。

您也可以使用悲觀並行,在 SELECT...FOR UPDATE 陳述式讀取的資料上取得專屬鎖定。舉例來說,Transaction 1 會在提交時中止,因為 Transaction 2 在 Transaction 1 取得鎖定之前就已提交修改內容,導致發生衝突。不過,如果交易順序導致 Transaction 2 在 Transaction 1 取得鎖定後嘗試更新行銷預算,則 Transaction 2 會等待 Transaction 1 提交並釋放鎖定,然後才能繼續。悲觀並行選項會將資料存取作業序列化。

詳情請參閱「並行控制」。

寫入-寫入衝突和正確性

使用可重複讀取隔離等級時,只有在沒有衝突的情況下,對相同資料的並行寫入作業才會成功。

在下列範例中,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 列。

後續步驟