【実務・中級編】 悲観的ロック – Cloud Spanner

Cloud Spannerの「悲観的ロック」を制する者が、高負荷システムを制する

Cloud Spannerを単なる「SQLが動く分散DB」だと思っているなら、今すぐその認識を改めたほうがいい。Spannerの真価は、分散環境でありながら「外部整合性(External Consistency)」を極限まで保証している点にある。

多くのエンジニアが「Spannerは楽観的ロック(OCC)が基本だから、競合が起きればリトライすればいい」と安易に考えがちだ。しかし、高頻度で更新が集中する「ホットスポット」において、リトライの嵐はレイテンシの死を意味する。

今回は、Spannerにおける「悲観的ロック(Pessimistic Locking)」の真髄と、それを実務の最前線でどう使いこなすべきか、現場の視点から切り込む。

—

1. なぜSpannerで「悲観的」が必要なのか

Spannerのトランザクションは、デフォルトでは楽観的制御で動く。書き込みを行う際、コミット直前に他の競合がないかチェックし、あればトランザクションをアボート(失敗)させる。

しかし、特定のキーに更新が集中するシステム(例:在庫管理、口座残高、ランキングのポイント加算)では、楽観的制御は「高コストなリトライ」を量産する。「どうせ競合するなら、最初からロックしてしまえ」というアプローチが悲観的ロックだ。

悲観的ロックの仕組み:SELECT FOR UPDATE

Spannerでは、SQLの `SELECT … FOR UPDATE` 句を使用することで、読み込みと同時に行ロックを確保できる。

— 悲観的ロックの例: 残高の更新
— 読み込んだ瞬間に当該レコードをロックし、他トランザクションの介入を防ぐ
SELECT balance FROM accounts
WHERE account_id = ‘user_123’
FOR UPDATE;

このクエリが実行されると、Spannerはトランザクションが終了(Commit/Rollback)するまで、当該行への排他ロックを保持する。後続の書き込みは、このロックが解放されるまで待機する。

—

2. 実務で遭遇する「罠」と設計パターン

悲観的ロックは強力だが、使い所を誤るとシステム全体を麻痺させる。設計レビューで私がよく指摘するポイントを挙げる。

① ロックの粒度と範囲を最小化せよ

「とりあえず広範囲をロックする」のは最悪だ。ロック期間が長引けば、DBの並行性が著しく低下する。

  • Bad: トランザクションの冒頭で大量のデータをSELECT FOR UPDATEする。
  • Good: 更新に必要な最小限のキーのみをロックし、外部API呼び出しや重い計算はロック取得前(またはロック外)に済ませる。

② デッドロックの回避

Spannerは内部でデッドロック検知を行うが、アプリケーション側で「ロック取得順序」を厳密に定義しておくのはエンジニアの基本教養だ。常にプライマリキーの昇順でロックを取るなど、ルールをコードベースに強制せよ。

—

3. 実践:高負荷環境での堅牢な実装

以下は、ある程度の競合が予想される環境での実装例だ。単にSQLを投げるだけでなく、「ロックが取れない場合のタイムアウト」を考慮するのがプロの仕事である。

// Cloud Spanner Java Clientによる悲観的ロックの実装イメージ
databaseClient.readWriteTransaction().run(transactionContext -> {
// 1. FOR UPDATEでロックを確保
ResultSet rs = transactionContext.executeQuery(
Statement.newBuilder(“SELECT balance FROM accounts WHERE id = @id FOR UPDATE”)
.bind(“id”).to(“user_123”)
.build());

// 2. ビジネスロジック
long balance = …;
long newBalance = balance – amount;

// 3. 更新
transactionContext.buffer(
Mutation.newUpdateBuilder(“accounts”)
.set(“id”).to(“user_123”)
.set(“balance”).to(newBalance)
.build());

return null;
});

ここでの注意点:
悲観的ロックを使用しても、Spannerのトランザクションは「ネットワーク遅延」の影響を受ける。ロックを保持したまま重い処理を挟むと、その間、他のトランザクションがすべて待たされ、`lock wait timeout` が頻発する。「ロック区間は極限まで短く」。これが鉄則だ。

—

4. チーフアーキテクトからの提言

「悲観的ロックを使えば競合問題は解決する」という考えは半分正解だが、半分は危険だ。

1. スケーラビリティのトレードオフ: 悲観的ロックは並列実行を直列化する。スループットが頭打ちになる限界を、設計段階で「負荷試験(Locust等)」によって把握しておくこと。
2. 代替案の検討: もし悲観的ロックで性能が出ないなら、そもそも「単一レコードの更新」という設計自体がボトルネックになっている可能性がある。その場合は「イベントソーシング」や「CQRS」への移行を検討すべきタイミングだ。

まとめ

  • 競合が頻発するクリティカルなパスには悲観的ロックを活用せよ。
  • ロック範囲は最小限に。ロック中には重い処理を一切書くな。
  • 悲観的ロックは「最後の手段」であると心得よ。

Spannerは、君が正しく設計すれば、世界中のどんなトラフィックにも耐えうる強靭なバックボーンになる。ロックの概念を正しく理解し、堅牢なシステムを構築してほしい。

次は君の設計が、この凄まじいエンジンをどう活かすのか。楽しみにしている。

コメント

タイトルとURLをコピーしました