分離レベルの概要

このページでは、さまざまな分離レベルと、Spanner での動作について説明します。

分離レベルは、同時実行トランザクションにどのデータが表示されるかを定義するデータベース プロパティです。Spanner は、ANSI / ISO SQL 標準で定義されている分離レベルのうち、シリアル化可能と反復可能な読み取りの 2 つをサポートしています。トランザクションを作成するときは、トランザクションに最適な分離レベルを選択する必要があります。選択した分離レベルにより、個々のトランザクションでレイテンシ、中止率、アプリケーションがデータ異常の影響を受けやすいかどうかなど、さまざまな要素を優先させることができます。最適な選択は、ワークロードの具体的な要件によって異なります。

シリアル化可能な分離

シリアル化可能な分離は、Spanner のデフォルトの分離レベルです。シリアル化可能な分離では、Spanner はトランザクションに対する最も厳格な同時実行制御を保証します。これは外部整合性と呼ばれます。Spanner は、単一サーバー データベースよりもパフォーマンスと可用性を高めるために複数のサーバー(場合によっては複数のデータセンター)でトランザクションを実行している場合でも、すべてのトランザクションが順次実行されるかのように動作します。また、1 つのトランザクションが完了してから別のトランザクションの commit が開始する場合、Spanner は、クライアントが常にトランザクションの結果を順番に確認できるようにします。直感的には、Spanner は単一マシンのデータベースに似ています。

トレードオフとして、シリアル化可能なトランザクションの基本的な性質により、ワークロードで読み取り / 書き込みの競合が多い場合(多くのトランザクションが別のトランザクションで更新中のデータを読み取る場合)、Spanner はトランザクションを中止する可能性があります。ただし、運用データベースではこれが適切なデフォルトです。これにより、通常は同時実行数が多い場合にのみ発生するタイミングの問題を回避できます。これらの問題は再現とトラブルシューティングが困難です。したがって、シリアル化可能な分離はデータ異常に対する最も強力な保護策となります。トランザクションの再試行が必要な場合、トランザクションの再試行によりレイテンシが増加する可能性があります。

反復可能な読み取りの分離

Spanner では、スナップショット分離と呼ばれる一般的な手法を使用して、反復可能な読み取りの分離を実装しています。Spanner の反復可能な読み取りの分離により、トランザクション内のすべての読み取りオペレーションは、トランザクションの開始時点でのデータベースの一貫性のある(強整合性)スナップショットを参照します。また、同じデータに対する同時書き込みは、競合がない場合にのみ成功することが保証されます。このアプローチは、多数のトランザクションが他のトランザクションが変更する可能性のあるデータを読み取ったり、読み取り / 書き込みの競合の可能性が高いシナリオで有効です。固定のスナップショットを使用することで、反復可能な読み取りは、より制限されたシリアル化可能な分離レベルのパフォーマンスへの影響を回避します。読み取りはロックを取得せずに実行でき、同時書き込みをブロックしないため、シリアル化の競合が原因で再試行が必要になる可能性のあるトランザクションの中止が少なくなります。クライアントがすでに読み取り / 書き込みトランザクションですべてを実行しており、読み取り専用トランザクションを再設計して使用することが難しいユースケースでは、反復可能な読み取りの分離を使用してワークロードのレイテンシを改善できます。

シリアル化可能な分離とは異なり、アプリケーションがデータベース スキーマで強制されない特定のデータ関係や制約に依存している場合(特にオペレーションの順序が重要である場合)、反復可能な読み取りではデータ異常が発生する可能性があります。このような場合、トランザクションはデータを読み取り、そのデータに基づいて決定を下します。データベース スキーマの制約が満たされている場合でも、アプリケーション固有の制約に違反する変更を書き込む可能性があります。これは、反復可能な読み取りの分離により、厳密なシリアル化なしで同時トランザクションを続行できるためです。潜在的な異常の 1 つに書き込みスキューがあります。これは、特定の種類の同時更新で発生します。各更新は個別に受け入れられますが、それらの組み合わせにより、アプリケーション データの完全性が損なわれます。たとえば、病院のシステムで、少なくとも 1 人の医師が常にオンコール状態である必要があり、医師はシフトのオンコールを解除するようリクエストできるとします。反復可能な読み取りの分離では、Richards 医師と Smith 医師の両方が同じシフトでオンコールに予定されているときに、同時にオンコールから外れるリクエストを試みると、各リクエストは並行して成功します。これは、両方のトランザクションが、トランザクションの開始時にオンコールが予定されている医師が少なくとも 1 人いることを読み取るためです。トランザクションが成功すると、データ異常が発生します。一方、シリアル化可能な分離では、シリアル化可能なトランザクションが潜在的なデータ異常を検出し、トランザクションを中止するため、これらのトランザクションが制約に違反することはありません。これにより、中止率は高くなりますが、アプリケーションの整合性を確保します。

前の例では、反復可能な読み取りの分離で SELECT FOR UPDATE 句を使用できます。SELECT…FOR UPDATE 句は、選択したスナップショットで読み取ったデータが commit 時に変更されていないかどうかを確認します。同様に、書き込みの整合性を確保するために内部でデータを読み取る DML ステートメントミューテーションも、commit 時にデータが変更されていないことを検証します。

詳細については、反復可能な読み取りの分離を使用するをご覧ください。

使用例

次の例は、反復可能な読み取りの分離を使用してロック オーバーヘッドを排除するメリットを示しています。Transaction 1Transaction 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 2Transaction 1 が開始した後、commit する前にスナップショットのタイムスタンプを確立します。Transaction 1 はデータを更新していないため、Transaction 2SELECT クエリは 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 が commit された後も 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 1 がスナップショット T1 を確立した後に Transaction 2 が commit したためです。反復可能な読み取りを使用すると、Transaction 2Transaction 1 によって読み取られたデータを変更しても、Transaction 1 は中止する必要がありませんでした。ただし、Spanner が返す結果が意図した結果になるとは限りません。

読み取り / 書き込みの競合と正確性

前の例では、Transaction 1SELECT ステートメントによってクエリされたデータが後続のマーケティング予算の決定に使用された場合、正確性の問題が発生する可能性があります。

たとえば、合計予算が 400,000 であるとします。Transaction 1SELECT ステートメントの結果に基づいて、予算に 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 2 が残りの 100,000 予算のうち 50,000 を新しいアルバム AlbumId = 5 にすでに割り当てていても、Transaction 1 は正常に commit されます。

SELECT...FOR UPDATE 構文を使用すると、トランザクションの存続期間中にトランザクションの特定の読み取りが変更されていないことを検証して、トランザクションの正確性を保証できます。次の例では、SELECT...FOR UPDATE を使用すると、Transaction 1 は commit 時に中止されます。

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 は待機や中止なしで正常に commit します。

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 が commit された後も 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 2AlbumId = 5 行への挿入をすでに commit しているため、Transaction 1 は中止されます。

次のステップ