【実務・中級編】 ロック管理の仕組み – Cloud Spanner

Cloud Spannerのロック戦略:分散環境で「整合性」と「スループット」を両立させる極意

Cloud Spannerを単なる「SQLが動く分散DB」だと思っているなら、今すぐその認識を改めた方がいい。Spannerの本質は、「TrueTimeによる外部整合性を保ちながら、いかにスループットを落とさずにロックを管理するか」という、分散システムにおける聖杯への挑戦にある。

今回は、我々エンジニアが避けては通れない「ロック管理」の深淵に切り込む。ここを理解していない設計は、高負荷時に必ずシステムを停止させる。

—

1. Spannerのロックモデル:厳格な「2相ロック」の正体

Spannerのトランザクションは、厳密な2相ロック(2PL)に基づいている。これは、トランザクションが終了するまで(コミットまたはアボートまで)ロックを保持し続けるという、伝統的かつ極めて安全な手法だ。

共有ロック(Sロック)と排他ロック(Xロック)

  • 共有ロック(Shared Lock): 読み取りに使用される。複数のトランザクションが同時に同じ行を読み取れる。
  • 排他ロック(Exclusive Lock): 書き込み(INSERT/UPDATE/DELETE)に使用される。ロックを保持している間、他のトランザクションはその行を読み取ることも書き込むこともできない。

ここで陥りやすい罠:
「読み取りならロックは関係ないだろう」という考えは捨てろ。Spannerで `SELECT` を行う際、明示的に `READ_ONLY` トランザクションを指定しなければ、デフォルトの `READ_WRITE` トランザクションとして扱われ、共有ロックを取得しようとする。この無駄なロック取得が、高負荷時のパフォーマンスを劇的に劣化させる主因だ。

—

2. デッドロックの発生メカニズム:なぜ「予測不能」なのか

Spannerは分散環境だ。単一のサーバーであればロックテーブルを管理すればいいが、Spannerでは行が複数のスプリット(Split)に分割され、異なるノードで管理されている可能性がある。

デッドロックは通常、複数のリソースを異なる順序で取得しようとした時に発生する。

— トランザクション A
UPDATE Users SET balance = balance – 100 WHERE id = ‘user1’; — 行1をXロック
UPDATE Users SET balance = balance + 100 WHERE id = ‘user2’; — 行2をXロック

— トランザクション B
UPDATE Users SET balance = balance – 100 WHERE id = ‘user2’; — 行2をXロック
UPDATE Users SET balance = balance + 100 WHERE id = ‘user1’; — 行1をXロック

このとき、Aは行2を、Bは行1を待ち続け、永遠に終わらない。Spannerは内部でデッドロック検出を行い、片方を強制的にアボートさせるが、「アボート後のリトライコスト」はアプリケーション側で負担しなければならない。

—

3. ロックの衝突を最小化する設計指針

「ロック管理」を極めるために、以下の3つの鉄則を守れ。

① 「読み取り」と「書き込み」を徹底的に分離せよ

読み取り専用のクエリに `READ_WRITE` トランザクションを使うのは罪だ。`Snapshot Read` を活用せよ。スナップショット読み取りはロックを取得しないため、書き込みと衝突せず、スループットを制限しない。

// 正しいSnapshot Readの例
Timestamp bound = Timestamp.now();
try (ResultSet rs = dbClient.singleUse(TimestampBound.ofReadTimestamp(bound))
.executeQuery(Statement.of(“SELECT FROM Users”))) {
// ロックなしで読み取れるため、書き込みトランザクションと競合しない
}

② ロック取得順序を厳格に固定せよ

アプリケーション層でロックするキー(行)に順序を持たせる。例えば、「IDの昇順でロックを取得する」というルールを強制するだけで、デッドロックの9割は回避できる。

③ トランザクションのスコープを極限まで絞れ

トランザクション内で「外部APIの呼び出し」や「重い計算」を挟むのは厳禁だ。ロックを保持している時間が長くなればなるほど、競合確率は指数関数的に上昇する。

  • 悪い例: `トランザクション開始 -> API通信 -> DB更新 -> コミット`
  • 良い例: `計算・API通信 -> トランザクション開始 -> DB更新 -> コミット`

—

4. 最後に:エンジニアへのメッセージ

Spannerにおいて、ロックは「悪」ではない。「整合性を担保するための必要経費」だ。

パフォーマンスが出ないからといって、不用意にロックを回避しようとして整合性を壊すのは、エンジニアとして最もやってはならないことだ。まずは `Cloud Monitoring` で `Lock Wait Time` を監視し、どこで競合が起きているのかを可視化せよ。

「なぜロックされているのか?」を突き詰め、トランザクションの粒度を見直し、アクセスの順序を整理する。この泥臭いチューニングこそが、世界最高峰の分散データベースを使いこなす唯一の道だ。

次のレビューで、君たちが「ロックの衝突を前提とした美しい設計」を見せてくれることを期待している。健闘を祈る。

コメント

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