このページでは、Spanner のトランザクションについて説明し、トランザクションを実行するためのサンプルコードを紹介します。
はじめに
Spanner 内のトランザクションは、データベース内の列、行、およびテーブルにまたがる時間内の単一論理ポイントにおいてアトミックに実行される読み取りと書き込みのセットです。
Spanner は、次のトランザクション モードをサポートしています。
ロック型読み取り / 書き込み。これらのトランザクションは悲観的ロックに依存し、必要に応じて、2 フェーズ commit を行います。ロック型読み取り / 書き込みトランザクションは中止されることがあり、その場合、アプリケーションの再試行が必要になります。
読み取り専用。このトランザクション タイプでは、複数の読み取りにまたがって整合性が保証されますが、書き込みは許されていません。デフォルトでは、読み取り専用トランザクションは、システムによって選択されたタイムスタンプで外部整合性を保証して実行されますが、過去のタイムスタンプで読み取るように構成することもできます。読み取り専用トランザクションは、commit する必要がなく、ロックされることもありません。また、読み取り専用トランザクションは、進行中の書き込みが完了するまで待機してから、実行する場合があります。
パーティション化された DMLこのトランザクション タイプでは、データ操作言語(DML)のステートメントをパーティション化された DML として実行します。パーティション化された DML は、一括更新と削除、特に定期的なクリーンアップとバックフィル用に設計されています。多数の盲目的書き込みを commit する必要があるが、アトミック トランザクションは必要ない場合は、バッチ書き込みを使用して Spanner テーブルを一括変更できます。詳細については、バッチ書き込みを使用してデータを変更するをご覧ください。
このページでは、Spanner のトランザクションの一般的なプロパティおよびセマンティクスについて説明し、Spanner のトランザクション(読み取り / 書き込み、読み取り専用、パーティション化された DML)のインターフェースについて紹介します。
読み取り / 書き込みトランザクション
以下のシナリオでは、ロック型読み取り / 書き込みトランザクションを使用する必要があります。
- 1 つ以上の読み取りの結果に依存する書き込みを行う場合、それらの読み取り / 書き込みは、同一の読み取り / 書き込みトランザクション内で行う必要があります。
- 例: 銀行口座 A の残高を倍にする。A の残高の読み取りは、残高を倍の値で置き換える書き込みと同一のトランザクション内で行う必要があります。
- アトミックに commit する必要がある 1 つ以上の書き込みを行う場合、それらの書き込みは同一の読み取り / 書き込みトランザクション内で行う必要があります。
- 例: 口座 A から口座 B に $200 を振り込む。2 つの書き込み(A で $200 を引く書き込みと、B で $200 を増やす書き込み)と、初期の口座残高の読み取りは、同一トランザクション内で行う必要があります。
- 1 つ以上の読み取りの結果に応じて 1 つ以上の書き込みを行う可能性がある場合、最終的には書き込みの実行が必要にならないとしても、これらの読み取り / 書き込みは同一読み取り / 書き込みトランザクション内で行う必要があります。
- 例: 銀行口座 A の現在の残高が $500 より多ければ、銀行口座 A から銀行口座 B に $200 を振り込む。トランザクションには、A の残高の読み取りと、書き込みが含まれている条件文とを含める必要があります。
次に、ロック型読み書きトランザクションを使用するべきではないシナリオを示します。
- 読み取りしか行わず、単一読み取りメソッドを使用して読み取りを表現できる場合は、その単一読み取りメソッド、読み取り専用トランザクションを使うべきで。単一読み取りは、読み取り / 書き込みトランザクションとは異なり、ロックしません。
プロパティ
Spanner 内の読み取り / 書き込みトランザクションが、時間の単一論理ポイントで一連の読み取り / 書き込みをアトミックに実行します。また、読み書きを実行したタイムスタンプが実時間と一致し、直列化順序がタイムスタンプの順序と一致しています。
読み書きトランザクションを使用する理由ですが、読み書きトランザクションは、リレーショナル データベースの ACID 特性を備えています(実際には、Spanner の読み書きトランザクションは、従来のACIDより強力な保証を備えています。下のセマンティクス セクションを参照してください)。
分離
読み書きトランザクションと読み取り専用トランザクションの分離プロパティは次のとおりです。
読み取りと書き込みを行うトランザクション
一連の読み取り(またはクエリ)と書き込みを含むトランザクションが正常に commit されたときに取得される分離プロパティは次のとおりです。
- トランザクション内のすべての読み取りが、トランザクションの commit タイムスタンプで取得された一貫性のあるスナップショットを反映する値を返した。
- commit 時に空の行または範囲が残っている。
- トランザクション内のすべての書き込みは、トランザクションの commit タイムスタンプで commit された。
- 書き込みは、トランザクションが commit されるまで、どのトランザクションにも認識されなかった。
特定の Spanner クライアント ドライバには、一時的なエラーをマスクするためのトランザクション再試行ロジックが含まれています。これは、トランザクションを再実行してクライアントで観測されたデータを検証することによって行われます。
すべての読み取りおよび書き込みが、トランザクション自体の視点からも、Spanner データベースの他のリーダーおよびライターの視点からも、単一の時点で発生しているように見えるという効果があります。つまり、読み込みと書き込みは同じタイムスタンプで終了します(下記のシリアル化可能性と外部整合性セクションで、これを図式化してあります)。
読み取りのみを行うトランザクション
読み取りのみを行う読み書きトランザクションの保証は似ています。行が存在しない場合でも、そのトランザクション内のすべての読み取りは、同一のタイムスタンプからデータを返します。異なるのは、データを読み取り、後で書き込みを行わずに読み取り / 書き込みトランザクションを commit する場合、読み取りを行ってから commit までの間にデータベースのデータが変化しなかったことが保証されないことです。最後に読み取ってからデータが変化したかどうかを知りたい場合の最善のアプローチは、(読み取り / 書き込みトランザクションまたは強力な読み取りを使用して)データを再び読み取ることです。また、効率性のため、読み取りのみを行い、書き込みを行わないことが事前にわかっている場合は、読み取り / 書き込みトランザクションではなく読み取り専用トランザクションを使用する必要があります。
アトミック性、整合性、耐久性
Spanner は、分離プロパティに加えて、アトミック性(トランザクション内の書き込みの 1 つが commit すると、すべての書き込みが commit したことになる)、整合性(トランザクションの終了後、データベースは整合性のある状態を保つ)、および耐久性(commit されたデータは commit された状態を保つ)を備えています。
これらのプロパティの利点
これらのプロパティのため、アプリケーションのデベロッパーは、同時に実行される可能性がある他のトランザクションからその実行を保護することに煩わされることなく、独自に各トランザクションの正確さのみにフォーカスできます。
インターフェース
Spanner クライアント ライブラリは、トランザクションが中止した場合は再試行をしながら、読み取り / 書き込みのトランザクションのコンテキストにおける一連の作業を実行するためのインターフェースを提供します。このようなコンテキストの例として、Spanner のトランザクションは commit するまで何回か再試行しなくてはならない場合があります。たとえば、2 つのトランザクションが同時に特定のデータのオペレーションを試みる場合、Spanner は一方のトランザクションをアボートさせて、他方のトランザクションが先に進むことができるようにします(更に稀ですが、Spanner 内の一時的なイベントがトランザクションをアボートさせる原因になることもあります)。トランザクションはアトミックなので、アボートさせられたトランザクションによってデータベースが目に見えて変化することはありません。したがって、トランザクションは成功するまで再試行を繰り返す必要があります。
Spanner クライアント ライブラリでトランザクションを使用する場合、トランザクションの本文(データベース内の 1 つ以上のテーブルに対する読み取りと書き込み)を関数オブジェクトの形式で定義します。Spanner クライアント ライブラリ内部では、トランザクションが commit するまで、または再試行不可能なエラーが発生するまで、関数の実行を繰り返します。
例
たとえば、スキーマとデータモデルのページに示されている Albums
テーブルに MarketingBudget
列を追加したとします。
CREATE TABLE Albums ( SingerId INT64 NOT NULL, AlbumId INT64 NOT NULL, AlbumTitle STRING(MAX), MarketingBudget INT64 ) PRIMARY KEY (SingerId, AlbumId);
マーケティング部門は Albums (1, 1)
をキーとするアルバムのマーケティングを push することを決定し、Albums
(2, 2)
の予算から $200,000 を移す(ただし、そのアルバムの予算からその金額を使用できる場合に限り)ように依頼しました。このオペレーションでは、トランザクションは、読み取りの結果、書き込みを行う可能性があるので、ロック型読み取り / 書き込みトランザクションを使用する必要があります。
以下では、読み書きトランザクションを実行する方法を示します。
C++
C#
Go
Java
Node.js
PHP
Python
Ruby
セマンティクス
直列化可能性と外部整合性
Spanner は「シリアル化可能性」を備えています。これは、異なるトランザクションの読み取り、書き込み、その他のオペレーションの一部が実際には並列に発生したとしても、すべてのトランザクションが直列に実行されるように見えることを意味しています。Spanner は、commit したトランザクションの順序を反映した commit タイムスタンプを割り当てます。実際には、Spanner は外部整合性と呼ばれるシリアル化可能性より強い保証を備えています。すなわち、トランザクションの commit 順序は commit タイムスタンプに反映され、これらの commit タイムスタンプは「リアルタイム」であるため、実時間と比較できます。トランザクション内の読み取りでは、トランザクション commit 前に commit されたすべてのものを認識でき、トランザクション commit 後に開始されたすべてのものが書き込みを認識できます。
たとえば、下の図で示した 2 つのトランザクションの実行について考えます。
青色のトランザクション Txn1
は、データ A
を読み取り、A
への書き込みをバッファリングして、正常に commit します。緑色のトランザクション Txn2
は Txn1
の後に開始し、データ B
を読み取った後、データ A
を読み取ります。Txn2
は Txn1
が A
への書き込みを commit した後に A
の値を読み取るので、Txn1
の完了前に Txn2
が開始したとしても、Txn1
による A
への書き込みの影響を Txn2
に与えます。
Txn1
と Txn2
の実行時間には多少のオーバーラップがあるものの、それらの commit タイムスタンプである c1
と c2
は線形的なトランザクション順序に従います。つまり、Txn1
の読み取りと書き込みのすべての効果が単一の時点(c1
)で発生したように見え、Txn2
の読み取りと書き込みのすべての効果が単一の時点(c2
)で発生したように見えます。さらに、c1 < c2
となります(これは、Txn1
と Txn2
の両方が書き込みを commit することで保証されます。これは書き込みが異なるマシンで実行された場合でも、同様に保証されます)。これは Txn1
の順序が Txn2
よりも前になることを意味します(ただし、トランザクションで Txn2
が読み取りのみを行った場合は、c1 <= c2
となります)。
読み取りでは commit 履歴のプレフィックスを監視します。読み取りに Txn2
が影響を及ぼした場合、Txn1
も影響を及ぼします。正常に commit したすべてのトランザクションにこの特性があります。
読み取りと書き込みの保証
トランザクション実行の呼び出しが失敗した場合、読み取りと書き込みが保証されるかどうかは、下位の commit 呼び出しが失敗する原因となったエラーのタイプに依存します。
たとえば、「行が見つかりません」、「行はすでに存在します」などのエラーは、バッファリングされた変異の書き込みで、クライアントがアップデートを試みた行が存在しないなど、なんらかのエラーに遭遇したことを意味します。このような場合、読み取りは整合性があることが保証され、書き込みは該当せず、行の不在は読み取りと整合性があることが保証されます。
トランザクション オペレーションのキャンセル
非同期読み取りオペレーションは、トランザクション内の他の既存のオペレーションに影響を与えることなく、ユーザーがいつでも(たとえば、高位のオペレーションをキャンセルしたり、最初の読み取りから受け取った結果に基づいて、読み取りを停止することを決定したとき)キャンセルできます。
ただし、読み取りのキャンセルを試みたとしても、Spanner によって読み取りが実際にキャンセルされることが保証されるわけではありません。読み取りのキャンセルを依頼しても、その読み取りが正常に終了したり、他の理由(アボートなど)で失敗することもあり得ます。さらに、そのキャンセルした読み取りが実際になんらかの結果を返してきて、その完了していない可能性のある結果が、トランザクションの commit の一部として確認されることもあり得ます。
読み取りとは異なり、トランザクションの commit オペレーションをキャンセルすると、トランザクションをアボートすることになることに注意してください(トランザクションがすでに別の理由で commit した場合、または失敗した場合を除く)。
パフォーマンス
ロック
Spanner では、複数のクライアントが同時に同一データベースと対話できます。Spanner は、複数の並列トランザクションの整合性を保証するために、共有的ロックと排他的ロックの組み合わせを使用して、データへのアクセスを制御します。トランザクションの一環で読み取りを実行すると、Spanner は共有的読み取りロックを取得するので、トランザクションが commit する準備ができるまで、他の読み取りは相変わらずデータにアクセスできます。トランザクションが commit し、書き込みに適用すると、トランザクションは排他的ロックへのアップグレードを試みます。データに対する新しい共有読み取りロックをブロックし、既存の共有読み取りロックが解除されるのを待ってから、データへの排他アクセスのための排他ロックを設定します。
ロックについての注意事項:
- ロックは、行および列の粒度で実施されます。トランザクション T1 が行「foo」の列「A」をロックしているときに、トランザクション T2 が行「foo」の列「B」に書き込んでも、競合は発生しません。
- 書き込む前のデータを読み取らない、データ項目への書き込み(「盲目的書き込み」と呼ばれることもある)は、同一項目に対する他の盲目的ライターと競合しません(各書き込みの commit タイムスタンプが、書き込みがデータベースに適用される順序を決定します)。そのため、書き込むデータをすでに読み取っている場合は、Spanner には排他的ロックへアップグレードすることのみが必要になります。それ以外の場合、Spanner はライター共有的ロックと呼ばれる共有的ロックを使用します。
- 読み取り / 書き込みトランザクション内で行検索を実行する場合は、セカンダリ インデックスを使用して、スキャンされる行の範囲を絞ります。これにより、Spanner によりロックされるテーブルの行数が少なくなり、範囲外の行を同時に変更できるようになります。
Spanner 以外のリソースへの排他的なアクセスを確保するために、ロックを使用しないでください。たとえば、インスタンスのコンピューティング リソース周辺でのデータの移動を可能にしたときなど、いくつかの理由で、Spanner によりトランザクションが中断される場合があります。トランザクションが再試行された場合、アプリケーション コードによって明示的に試行されたか、Spanner JDBC ドライバなどのクライアント コードによって暗黙に試行されたかにかかわらず、トランザクションの試行が実際に行われている間に、ロックが実施されたことのみが保証されます。
ロックの統計情報のイントロスペクション ツールを使用して、データベースでロックの競合を調査できます。
デッドロックの検出
Spanner は、複数のトランザクションがデッドロックに陥る可能性を検出し、1 つを除くすべてのトランザクションを強制的に中止します。たとえば、次のシナリオについて考えてみましょう。トランザクション Txn1
はレコード A
のロックを保持しており、レコード B
がロックできる状態になるのを待っています。Txn2
はレコード B
のロックを保持しており、レコード A
がロックできる状態になるのを待っています。このような状況を打開するための唯一の方法は、一方のトランザクションを中止し、そのロックを解放することで、もう一方のトランザクションの処理を行うことです。
Spanner は、標準的な「wound-wait」アルゴリズムを使用して、デッドロックの検出を処理します。Spanner は、競合するロックをリクエストする各トランザクションの経過時間を内部で追跡します。さらに、古いトランザクションが後から開始したトランザクションを中止できるようにします(「古い」トランザクションとは、トランザクション内で最も早い読み取り、クエリ、commit が、より早く発生したトランザクションを意味します)。
Spanner は、古いトランザクションを優先することで、すべてのトランザクションが、時間の経過に伴い、他のトランザクションよりも古くなって優先順位が高くなることで、ロックを取得できるようにしています。たとえば、リーダー共有的ロックを取得しているトランザクションが、ライター共有的ロックを必要とする、より古いトランザクションによって中止されることがあります。
分散実行
Spanner は、複数のサーバーにまたがるデータに対してトランザクションを実行できます。この機能を実現することで、単一サーバー トランザクションと比較して、パフォーマンスが低くなります。
分散できるトランザクションのタイプSpanner は、内部的に、多くのサーバー間でデータベースの行の責任を分割できます。行と、それに対応するインターリーブ テーブル内の行は、近隣のキーを持つ同一テーブル内の 2 行として、通常、同じサーバーによってサービスを提供されます。Spanner は、さまざまなサーバー上の行にまたがるトランザクションを実行できます。ただし、経験則として、同じ場所に配置された多くの行に関わるトランザクションは、データベース全体または大きなテーブル全体に散在する多くの行に関わるトランザクションよりも高速かつ低コストです。
Spanner のトランザクションとしては、アトミックに適用される読み取りと書き込みのみが含まれるものが最も効率的です。すべての読み取りと書き込みがキースペースの同じ部分にあるデータにアクセスする場合に、トランザクションは最も速くなります。
読み取り専用トランザクション
Spanner は、ロック型読み書きトランザクションに加えて、読み取り専用トランザクションも備えています。
同一タイムスタンプで複数の読み取りを実行する必要がある場合、読み取り専用トランザクションを使用してください。読み取りを Spanner の単一読み取りメソッドで表現できる場合、代わりにその単一読み取りメソッドを使用する必要があります。このような単一読み取りメソッドを使用した場合のパフォーマンスは、読み取り専用トランザクションで実行される単一の読み取りのパフォーマンスに匹敵する必要があります。
大量のデータを読み取る場合は、パーティションを使用してデータを並列で読み取ることを検討してください。
読み取り専用トランザクションは書き込まないので、ロックを保持することはなく、他のトランザクションをブロックすることもありません。読み取り専用トランザクションはトランザクションの commit 履歴の整合性のあるプレフィックスを監視しているので、アプリケーションは常に整合性のあるデータを取得できます。
プロパティ
Spanner の読み取り専用トランザクションは、読み取り専用トランザクション自体の視点からも、Spanner データベースの他のリーダーおよびライターの両方の視点からも、時間的に単一の論理ポイントで、一連の読み取りを実行します。これは、読み取り専用トランザクションは、トランザクション履歴の一定の時点のデータベースの整合性のある状態を常に監視していることを意味します。
インターフェース
Spanner は、トランザクションが中止した場合は再試行をしながら、読み取り専用トランザクションのコンテキストにおける一連の作業を実行するためのインターフェースを提供します。
例
以下は、読み取り専用トランザクションを使用して、同一のタイムスタンプの 2 つの読み取りで整合性のあるデータを取得する方法を示しています。
C++
C#
Go
Java
Node.js
PHP
Python
Ruby
パーティション化 DML トランザクション
パーティション化されたデータ操作言語(パーティション化 DML)を使用すると、大規模な UPDATE
ステートメントや DELETE
ステートメントを実行できます。実行中に、トランザクション数の上限に達することも、テーブル全体がロックされることもありません。Spanner はキースペースをパーティション化し、別々の読み取り / 書き込みトランザクションで各パーティションの DML ステートメントを実行します。
読み書きトランザクションで DML ステートメントを実行するには、ステートメントをコード内で明示的に作成します。詳しくは、DML の使用をご覧ください。
プロパティ
クライアント ライブラリのメソッドと Google Cloud CLI のいずれを使用する場合でも、パーティション化 DML ステートメントは一度に 1 つしか実行できません。
パーティション化されたトランザクションは、commit またはロールバックをサポートしていません。Spanner は、DML ステートメントをすぐに実行し、適用します。オペレーションをキャンセルした場合やオペレーションが失敗した場合、Spanner は実行中のすべてのパーティションをキャンセルし、残りのパーティションを開始しません。Spanner は、すでに実行されているパーティションをロールバックしません。
インターフェース
Spanner は、単一のパーティション化 DML ステートメントを実行するインターフェースを提供します。
例
次のコード例は、Albums
テーブルの MarketingBudget
列を更新します。
C++
ExecutePartitionedDml()
関数を使用して、パーティション化 DML ステートメントを実行します。
C#
ExecutePartitionedUpdateAsync()
メソッドを使用して、パーティション化 DML ステートメントを実行します。
Go
PartitionedUpdate()
メソッドを使用して、パーティション化 DML ステートメントを実行します。
Java
executePartitionedUpdate()
メソッドを使用して、パーティション化 DML ステートメントを実行します。
Node.js
runPartitionedUpdate()
メソッドを使用して、パーティション化 DML ステートメントを実行します。
PHP
executePartitionedUpdate()
メソッドを使用して、パーティション化 DML ステートメントを実行します。
Python
execute_partitioned_dml()
メソッドを使用して、パーティション化 DML ステートメントを実行します。
Ruby
execute_partitioned_update()
メソッドを使用して、パーティション化 DML ステートメントを実行します。
次のコード例では、SingerId
列に基づいて Singers
テーブルから行を削除します。