Cloud Spanner におけるトランザクションのロックについて
Google Cloud Japan Team
※この投稿は米国時間 2022 年 11 月 16 日に、Google Cloud blog に投稿されたものの抄訳です。
Cloud Spanner は、無制限のスケール、強整合性、最大 99.999% の可用性を備えたフルマネージド リレーショナル データベースです。 Cloud Spanner は、支払い処理やオンラインでのゲームプレイといった、データの読み取りや更新を同時に実行することが多いアプリケーション向けに設計されています。Cloud Spanner は、複数の同時実行トランザクションの整合性を確保するために、共有ロックと排他ロックの組み合わせを使用して、データへのアクセスを制御します。このブログでは、Cloud Spanner で発生するさまざまなロックの種類を紹介します。また、Cloud Spanner におけるトランザクションのロックの一般的なケースについても説明し、そうしたロックが発生する可能性を検出する場合の注意点についても取り上げます。
ロックとは
詳しい説明に入る前に、まずはデータベース システムの観点から、ロックについて簡単に振り返り定義します。
データベースにおける「ロック」とは、同時実行制御のメカニズムを指します。一般的にロックはリソース上で発生します。リソースは行、列、テーブルの場合もあれば、データベース全体の場合もあります。リソースがトランザクションでロックされている場合、ロックが解除されるまでは、別のトランザクションによるアクセスができなくなります。
トランザクションのタイムライン ビュー
読み取りトランザクションおよび読み取り / 書き込みトランザクションにおけるトランザクションのロックについて触れる前に、Spanner でのトランザクションのタイムライン ビューについて振り返るのが重要です。
後ほど、一般的なトランザクション パターンのセクションでトランザクションの書き込みバッファリングのコンセプトについても触れますので、覚えておいてください。書き込みのバッファリングでは、書き込みを受け付ける Cloud Spanner サーバーを参照します。これらの書き込みは、commit が実行されるまでは耐久性はありませんのでご注意ください。
Cloud Spanner におけるロックのタイプ
Cloud Spanner のオペレーションは、オペレーションが読み取り / 書き込みトランザクションに含まれるときにロックを取得します。読み取り専用トランザクションはロックを取得しません。テーブル全体や行全体をロックするその他のアプローチとは異なり、Spanner におけるトランザクションのロックの粒度は、セル、つまり行と列の交点となります。そのため、2 つのトランザクションで同じ行の異なる列を同時に読み取ることも、修正を行うことも可能となります。Cloud Spanner ではさまざまなロックモードを使用して、特定の時間に特定のデータセルにアクセスできるトランザクションの数を最大化します。
こちらは、異なるロックタイプについての簡単な説明です。各ロックタイプについての詳細は、Cloud Spanner のドキュメントをご確認ください。
ReaderShared ロック - 読み取り / 書き込みトランザクションがデータを読み取るときに取得されます。
WriterShared ロック - 読み取り / 書き込みトランザクションがデータを読み取らずに書き込むときに取得されます。
Exclusive ロック - すでに ReaderShared ロックを取得している読み取り / 書き込みトランザクションが、読み取りの完了後にデータの書き込みを試みるときに取得されます。Exclusive ロックは、ReaderShared ロックと WriterShared ロックを同時に保持するトランザクションの特殊なケースです。
WriterSharedTimestamp ロック - 主キーの一部としてトランザクションの commit タイムスタンプがある新しい行を挿入するときに取得される特殊なタイプです。
ロック競合への対応
読み取り / 書き込みトランザクションではアトミック(不可分)に実行されるロックを使用するため、デッドロックのリスクが生じます。たとえば、次のシナリオについて考えてみましょう。トランザクション Txn1 はレコード A のロックを保持しており、レコード B がロックできる状態になるのを待っています。Txn2 はレコード B のロックを保持しており、レコード A がロックできる状態になるのを待っています。このような状況を打開するための唯一の方法は、一方のトランザクションを中止し、そのロックを解放することで、もう一方のトランザクションの処理を行うことです。
Cloud Spanner は、標準的な「wound-wait」アルゴリズムを使用して、デッドロックの検出を処理します。Spanner は、競合するロックをリクエストする各トランザクションの経過時間を内部で追跡します。さらに、古いトランザクションが後から開始したトランザクションを中止できるようにします。「古い」とは、トランザクション内で最も早い読み取り、クエリ、commit が、より早く発生したトランザクションを意味します。
一般的なトランザクション パターン
Cloud Spanner におけるロックとトランザクションの基礎について確認したところで、いくつか実践的なユースケースを見ていきましょう。アカウントという名前が付いたテーブルで、ユーザーの口座残高をクエリおよび更新するアプリケーションの実例を確認していきます。アカウントのテーブルには次の列があります。
ケース 1: 優先度の高い共有ロックのために、排他ロックの取得を待っているトランザクション
シーケンス
Txn1 が開始されます。
Txn2 が開始されます。
Txn2 がテーブルを読み取り、ReadShared ロックを取得します。
Txn1 が書き込みをバッファします。
Txn1 は commit を試みますが、Txn2 の方が優先度が高いため(先に最初のオペレーションを実行しているため)、Txn1 は Txn2 が ReadShared ロックを解放するまで待つ必要があります。
commit の完了後に、Txn2 は ReadShared ロックを解放します。これで、Txn1 は Exclusive ロックへのアップグレードが可能となります。Txn1 が Exclusive ロックを取得して、commit します。
注 1: UPDATE WHERE
はいつでも SELECT WHERE
に変換できるため、UPDATE
ステートメントは盲目的書き込みではなく、読み取り / 書き込みオペレーションとなります。
注 2: 上記の例では、単一の行をクエリおよび更新する場合のユースケースを考慮していますが、同様のトランザクション パターンはキー範囲にも適用可能です。
注意すべき点
トランザクションがロックの取得を待つ間に発生するタイムアウト。トランザクションの統計情報とロックの統計情報を参照して、タイムアウトを検出する方法を把握しましょう。
タイムアウトが発生する典型的な場面
テーブルで単一のキー / キー範囲を同時更新する場合。
ケース 2: 同時実行の成功によるトランザクションの中断
シーケンス
Txn1 が開始されます。
Txn2 が開始されます。
Txn1 がテーブルから行を読み取り、ReadShared ロックを取得します。
Txn2 が Txn1 と同じ行を読み取り、ReadShared ロックを取得します。
Txn1 が書き込みをバッファします。
Txn2 が書き込みをバッファします。
Txn1 が commit を試みて、優先度も高いため(先に最初のオペレーションを実行しているため)、Exclusive ロックへのアップグレードを行う優先権を獲得します。Txn1 が Exclusive ロックを取得して、commit します。
Txn1 は commit をしていて、トランザクションの優先度も高いため、Txn1 は Txn2 を中断します。中断は、Txn2 が commit を試みる前に行われると考えられます。
注意すべき点
トランザクションの wound による多数のトランザクションの中断。トランザクションの統計情報とロックの統計情報を参照して、トランザクションの中断を検出する方法を把握しましょう。
トランザクションの中断が発生する典型的な場面
単一のキー / キー範囲を同時更新する場合。通常、テーブルにホットキーがある場合に発生します。
ケース 3: 優先度の高い共有ロックのために、前の成功している同時実行のトランザクションが中断され、排他ロックが取得できるのを待っているトランザクション
シーケンス
Txn1 が開始されます。
Txn2 が開始されます。
Txn2 がテーブルを読み取り、ReadShared ロックを取得します。
Txn1 がテーブルを読み取り、ReadShared ロックを取得します。
Txn1 が書き込みをバッファします。
Txn2 が書き込みをバッファします。
Txn1 が commit を試みますが、Txn2 が ReadShared ロックを保有しているため、クリアされるまで待つことになります。
Txn2 は優先度が高いため(先に最初のオペレーションを実行しているため)、先に Exclusive ロックを取得します。Txn2 は ReaderShared ロックを Exclusive ロックにアップグレードし、commit します。
最後に、ステップ 8 が完了した後で Txn1 が commit を試みると、(Txn2 の ReaderShared ロックがクリアされたため)中断されます。これは、優先度の高いトランザクション(Txn2)によるデッドロックを防ぐためです。
注: ケース 1 では、ケース 3 と同様に Txn2 が先に ReaderShared ロックを取得しますが、Exclusive ロックにアップグレードされることはありません(書き込みがないため)。そのため、Txn1 を中断する必要はなく、単純に Txn2 が ReaderShared ロックを解放するのを待ってから、commit するだけで問題ありません。
注意すべき点
トランザクション間の競合率の高さによる多数のトランザクションの中断。トランザクションの統計情報とロックの統計情報を参照して、トランザクションの中断を検出する方法を把握しましょう。
トランザクションの中断が発生する典型的な場面
単一のキー / キー範囲を同時更新する場合。通常、テーブルにホットキーがある場合に発生します。
推奨事項
こうした問題を軽減し、発生を抑制するために、次のベスト プラクティスの導入をおすすめします。
同一のタイムスタンプで 2 つ以上の読み取りを実行する必要があり、読み取りだけを行うことが事前にわかっている場合は、読み取り専用のトランザクションの使用を検討すると良いでしょう。読み取り専用トランザクションは書き込まないので、ロックを保持することはなく、他のトランザクションをブロックすることもありません。さらに、読み取り専用のトランザクションの場合、中断はありませんので、再試行ループでラップする必要はありません。
常に、最小限のキーまたはキー範囲のサブセットで ReadShared ロックを取得しましょう。これにより、ロックの競合の可能性が減少します。
トランザクション内の重要なパスコードのみを含むようにコードを分析しましょう。迅速なトランザクション プロセスを確保するための良い方法は、不必要なリモート呼び出しや、複雑で長時間実行されるビジネス ロジックを避けることです。
複数スプリットのトランザクションのニーズを分析します。トランザクションでは、2 つのフェーズの commit プロトコルを使用する 2 つ以上のスプリットを更新するため、より長期間ロックを保有することとなり、ロックの競合が生じる可能性が増加します。
使ってみる
Spanner の独特なアーキテクチャであれば、最新のリレーショナル データベースでデベロッパーが依存する整合性を損なうことなく、水平スケーリングが可能となります。今すぐ Spanner を 90 日間無料でお試しいただくか、月額 $65 でご利用ください。
- ソフトウェア エンジニア Manit Gupta