このページでは、シリアル化可能な分離で FOR UPDATE 句を使用する方法について説明します。
FOR UPDATE 句のロック メカニズムは、読み取り対象のデータを常に読み取る分離と直列化可能分離で異なります。シリアル化可能な分離を使用する場合、SELECT クエリを使用してテーブルをスキャンするときに FOR UPDATE 句を追加すると、行と列の粒度レベル(セルレベル)の交差で排他ロックが有効になります。ロックは、読み取り / 書き込みトランザクションが存続している間、保持されます。この間、FOR UPDATE 句により、現在のトランザクションが完了するまで、他のトランザクションがロックされたセルを変更できなくなります。
FOR UPDATE 句の使用方法については、GoogleSQL と PostgreSQL の FOR UPDATE リファレンス ガイドをご覧ください。
FOR UPDATE 句を使用する理由
分離レベルが厳しくないデータベースでは、同時実行トランザクションがデータの読み取りとトランザクションの commit の間でデータを更新しないように、FOR UPDATE 句が必要になる場合があります。Spanner はデフォルトでシリアル化可能性を適用するため、トランザクション内でアクセスされたデータが commit 時に古くない場合にのみ、トランザクションが正常に commit されることが保証されます。したがって、Spanner でトランザクションの正確性を確保するために FOR UPDATE 句は必要ありません。
ただし、書き込みの競合が多いユースケース(複数のトランザクションが同じデータを同時に読み書きしている場合など)では、同時実行トランザクションにより中断が増加する可能性があります。これは、複数のトランザクションが同時に共有ロックを取得し、排他的ロックへのアップグレードを試み、トランザクションがデッドロックを引き起こすためです。デッドロックが発生すると、各トランザクションが相手のトランザクションがリソースを解放するのを待機するため、トランザクションが永久にブロックされます。処理を進めるため、Spanner は 1 つを除くすべてのトランザクションを中止して、デッドロックを解消します。詳細については、ロックをご覧ください。
FOR UPDATE 句を使用するトランザクションは排他ロックを事前に取得して実行を続行しますが、他のトランザクションはロックの順番を待機します。競合するトランザクションは一度に 1 つしか実行できないため、Spanner ではスループットが制限される可能性があります。ただし、Spanner は 1 つのトランザクションの処理のみを進めるため、トランザクションの中止と再試行にかかる時間が短くなります。
したがって、同時書き込みリクエストのシナリオで中断されたトランザクションの数を減らすことが重要である場合は、FOR UPDATE 句を使用して、全体的な中断数を減らし、ワークロードの実行効率を高めることができます。
LOCK_SCANNED_RANGES ヒントとの比較
FOR UPDATE 句は、LOCK_SCANNED_RANGES=exclusive ヒントと同様の機能を果たします。
次のような主な違いが 2 つあります。
LOCK_SCANNED_RANGESヒントを使用すると、トランザクションはステートメント全体のスキャン範囲に対する排他ロックを取得します。サブクエリで排他ロックを取得することはできません。ロックヒントを使用すると、必要以上にロックが取得され、ワークロードでのロック競合につながる可能性があります。次の例は、ロックヒントの使用方法を示しています。@{lock_scanned_ranges=exclusive} SELECT s.SingerId, s.FullName FROM Singers AS s JOIN (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000) AS a ON a.SingerId = s.SingerId;一方、次の例に示すように、サブクエリで
FOR UPDATE句を使用できます。SELECT s.SingerId, s.FullName FROM Singers AS s JOIN (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000) FOR UPDATE AS a ON a.SingerId = s.SingerId;DML ステートメントでは
LOCK_SCANNED_RANGESヒントを使用できますが、SELECTステートメントではFOR UPDATE句のみを使用できます。
ロックのセマンティクス
同時書き込みリクエストと、デッドロックの結果としてトランザクションが中断されるコストを削減するため、Spanner は可能であればセルレベルでデータをロックします。セルレベルは、テーブル内の最も粒度の細かいデータレベルです。行と列の交点にあるデータポイントです。FOR UPDATE 句を使用すると、Spanner は SELECT クエリによってスキャンされる特定のセルをロックします。
次の例では、SingerId = 1 行と AlbumId = 1 行の MarketingBudget セルが Albums テーブルで排他的にロックされているため、このトランザクションが commit またはロールバックされるまで、同時実行トランザクションがそのセルを変更できなくなります。ただし、同時実行トランザクションは、その行の AlbumTitle セルを更新できます。
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId = 1
FOR UPDATE;
ロックされたデータの読み取りで同時実行トランザクションがブロックされる可能性がある
1 つのトランザクションがスキャン範囲の排他ロックを取得すると、同時実行トランザクションによってそのデータの読み取りがブロックされる可能性があります。Spanner はシリアル化可能性を適用するため、トランザクションの存続期間内に別のトランザクションによって変更されないことが保証されている場合にのみ、データを読み取ることができます。すでにロックされているデータを読み取ろうとする同時実行トランザクションは、ロックを保持しているトランザクションが commit、ロールバック、またはタイムアウトされるまで、待機しなければならない場合があります。
次の例では、Transaction 1 は 1 <= AlbumId < 5 の MarketingBudget セルをロックします。
-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 5
FOR UPDATE;
AlbumId = 1 の MarketingBudget の読み取りを試行している Transaction 2 は、Transaction 1 が commit されるかロールバックされるまでブロックされます。
-- Transaction 2
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId = 1;
-- Blocked by Transaction 1
同様に、FOR UPDATE でスキャン範囲をロックしようとするトランザクションは、重複するスキャン範囲をロックする同時実行トランザクションによってブロックされます。
次の例の Transaction 3 もブロックされます。これは、Transaction 1 が 3 <= AlbumId < 5 の MarketingBudget セルをロックしているためです。これは Transaction 3 と重複するスキャン範囲です。
-- Transaction 3
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 3 and AlbumId < 10
FOR UPDATE;
-- Blocked by Transaction 1
インデックスを読み込む
スキャンされた範囲をロックしたクエリがベーステーブル内の行をロックしても、同時実行トランザクションがインデックスから読み取る場合、同時実行読み取りがブロックされないことがあります。
次の Transaction 1 は、SingerId = 1 の SingerId セルと SingerInfo セルをロックします。
-- Transaction 1
SELECT SingerId, SingerInfo
FROM Singers
WHERE SingerId = 1
FOR UPDATE;
読み取り専用の Transaction 2 は、インデックス テーブルをクエリするため、Transaction 1 で取得されたロックによってブロックされません。
-- Transaction 2
SELECT SingerId FROM Singers;
同時実行トランザクションで、すでにロックされているデータに対する DML オペレーションがブロックされない
1 つのトランザクションが排他的ロックのヒントを使用してセル範囲のロックを確保した場合、ロックされたセルでデータを読み取らずに書き込みを実行しようとする同時実行トランザクションは処理を続行できます。ロックを保持しているトランザクションが commit またはロールバックされるまで、トランザクションは commit でブロックされます。
次の Transaction 1 は、1 <= AlbumId < 5 の MarketingBudget セルをロックします。
-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 5
FOR UPDATE;
Transaction 2 が Albums テーブルの更新を試みると、Transaction 1 が commit またはロールバックされるまで更新がブロックされます。
-- Transaction 2
UPDATE Albums
SET MarketingBudget = 200000
WHERE SingerId = 1 and AlbumId = 1;
> Query OK, 1 rows affected
COMMIT;
-- Blocked by Transaction 1
スキャン範囲がロックされると、既存の行とギャップがロックされる
1 つのトランザクションがスキャン範囲の排他ロックを取得すると、同時実行トランザクションはその範囲内のギャップにデータを挿入できません。
次の Transaction 1 は、1 <= AlbumId < 10 の MarketingBudget セルをロックします。
-- Transaction 1
SELECT MarketingBudget
FROM Albums
WHERE SingerId = 1 and AlbumId >= 1 and AlbumId < 10
FOR UPDATE;
Transaction 2 がまだ存在しない AlbumId = 9 の行を挿入しようとすると、Transaction 1 が commit またはロールバックされるまで、その行の挿入はブロックされます。
-- Transaction 2
INSERT INTO Albums (SingerId, AlbumId, AlbumTitle, MarketingBudget)
VALUES (1, 9, "Hello hello!", 10000);
> Query OK, 1 rows affected
COMMIT;
-- Blocked by Transaction 1
ロックの取得に関する注意事項
説明されているロック セマンティクスは一般的なガイダンスを提供しますが、Spanner が FOR UPDATE 句を使用するトランザクションを実行するときにロックがどのように取得されるかを保証するものではありません。Spanner のクエリ最適化メカニズムも、取得されるロックに影響する可能性があります。この句により、現在のトランザクションが完了するまで、他のトランザクションがロックされたセルを変更できなくなります。
クエリ構文
このセクションでは、FOR UPDATE 句を使用する場合のクエリ構文について説明します。
最も一般的な使用方法は、最上位の SELECT ステートメントです。次に例を示します。
SELECT SingerId, SingerInfo
FROM Singers WHERE SingerID = 5
FOR UPDATE;
このサンプルでは、SELECT ステートメントで FOR UPDATE 句を使用して、WHERE SingerID = 5 の SingerId セルと SingerInfo セルを排他的にロックする方法を示します。
WITH ステートメントで使用する
WITH ステートメントの外側レベルのクエリで FOR UPDATE を指定するときに、FOR UPDATE 句は WITH ステートメントのロックを取得しません。
次のクエリでは、ロックする意図が共通テーブル式(CTE)クエリに伝播されないため、Singers テーブルでロックは取得されません。
WITH s AS (SELECT SingerId, SingerInfo FROM Singers WHERE SingerID > 5)
SELECT * FROM s
FOR UPDATE;
CTE クエリで FOR UPDATE 句が指定されている場合、CTE クエリのスキャン範囲でロックが取得されます。
次の例では、SingerId > 5 がロックされている行の SingerId セルと SingerInfo セルがロックされています。
WITH s AS
(SELECT SingerId, SingerInfo FROM Singers WHERE SingerId > 5 FOR UPDATE)
SELECT * FROM s;
サブクエリで使用する
FOR UPDATE 句は、1 つ以上のサブクエリを含む外側レベルのクエリで使用できます。ロックは、最上位クエリとサブクエリ内で取得されます(式サブクエリを除く)。
次のクエリは、SingerId > 5. の SingerId セルと SingerInfo セルをロックします。
(SELECT SingerId, SingerInfo FROM Singers WHERE SingerId > 5) AS t
FOR UPDATE;
次のクエリは式サブクエリ内にあるため、Albums テーブルのセルはロックされません。式サブクエリから返された行の SingerId セルと SingerInfo セルはロックされます。
SELECT SingerId, SingerInfo
FROM Singers
WHERE SingerId = (SELECT SingerId FROM Albums WHERE MarketingBudget > 100000)
FOR UPDATE;
ビューのクエリに使用する
次の例に示すように、FOR UPDATE 句を使用してビューをクエリできます。
CREATE VIEW SingerBio AS SELECT SingerId, FullName, SingerInfo FROM Singers;
SELECT * FROM SingerBio WHERE SingerId = 5 FOR UPDATE;
ビューを定義するときに FOR UPDATE 句は使用できません。
サポートされていないユースケース
次の FOR UPDATE のユースケースはサポートされていません。
- Spanner の外部でコードを実行するための相互排除のメカニズムとして: Spanner のロックを使用して、Spanner の外部のリソースへの排他的アクセスを確保しないでください。トランザクションが Spanner によって中断される場合があります。たとえば、トランザクションが再試行された場合、アプリケーション コードによって明示的に試行されたか、Spanner JDBC ドライバなどのクライアント コードによって暗黙に試行されたかにかかわらず、トランザクションの試行が実際に行われている間にロックが実施されたことのみが保証されます。
LOCK_SCANNED_RANGESヒントと組み合わせる: 同じクエリでFOR UPDATE句とLOCK_SCANNED_RANGESヒントの両方を使用することはできません。使用すると、Spanner からエラーが返されます。- 全文検索クエリ: 全文検索インデックスを使用するクエリで
FOR UPDATE句を使用できません。 - 読み取り専用トランザクション:
FOR UPDATE句は、読み取り / 書き込みトランザクション内で実行されるクエリでのみ有効です。 - DDL ステートメント内: DDL ステートメント内のクエリで
FOR UPDATE句を使用できません。これらのクエリは、後で実行するために保存されます。たとえば、ビューを定義するときにFOR UPDATE句を使用できません。ロックが必要な場合は、ビューをクエリするときにFOR UPDATE句を指定できます。
次のステップ
- GoogleSQL と PostgreSQL で
FOR UPDATE句を使用する方法を学習する。 - 繰り返し読み取り分離で SELECT FOR UPDATE を使用する方法を確認する。
LOCK_SCANNED_RANGESヒントについて学習する。- Spanner のロックについて学習する。
- Spanner のシリアル化可能性について学習する。