分離レベルの概要

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

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

シリアル化可能な分離

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

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

Repeatable Read 分離

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

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

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

詳細については、繰り返し読み取り分離を使用するをご覧ください。

使用例

次の例は、繰り返し読み取り分離を使用してロック オーバーヘッドを排除するメリットを示しています。Transaction 1Transaction 2 の両方が Repeatable Read 分離で実行されます。

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 が開始してからコミットするまでの間にスナップショットのタイムスタンプを確立します。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 1 は、Transaction 2 が commit された後も継続されます。

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 2Transaction 1 がスナップショット T1 を確立した後にコミットしたためです。反復可能読み取りを使用すると、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 は正常にコミットされます。

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 が commit された後も継続されます。

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

次のステップ