【実務・中級編】 楽観的並行性制御 – Cloud Spanner

Cloud Spannerの「楽観的並行性制御」を極める:衝突を恐れず、スケールを勝ち取るアーキテクチャ

エンジニア諸君、設計レビューで「Cloud Spannerならスケーラブルだ」という言葉を安易に使っていないか?

Cloud Spannerは魔法の杖ではない。その真骨頂は、分散システムにおける「強整合性」と「水平スケーラビリティ」を両立させている点にある。そして、そのパフォーマンスを支える心臓部こそが「楽観的並行性制御(Optimistic Concurrency Control: OCC)」だ。

今回は、教科書的な説明は捨て、実務の戦場でこの特性をどう武器に変えるか、その極意を伝授しよう。

—

1. なぜ「悲観」ではなく「楽観」なのか

RDBMSの伝統的な悲観的ロック(`SELECT FOR UPDATE`など)は、リソースを確保し、他者を排除することで安全を担保する。だが、グローバルに分散するSpannerのノード間で、ミリ秒単位のロックを維持しようとすればどうなるか? ネットワークの遅延がそのまま「詰まり」となり、システム全体が麻痺する。

Spannerの楽観的並行性制御は違う。
「データは競合しないだろう」という前提で走り出し、コミットの瞬間にのみ「誰か書き換えたか?」を検証する。

  • メリット: 読み取りトランザクションはロックを一切消費せず、ノードを跨いだ広大なスケールを維持できる。
  • 代償: 衝突が多発すれば、コミット時に「Abort(中断)」が発生し、リトライコストが跳ね上がる。

2. 現場で直面する「Abort」の正体

多くのエンジニアがここで躓く。「なぜかたまにトランザクションが失敗する」という報告だ。これはバグではない。Spannerが書き込みの整合性を守るための「正常な自浄作用」だ。

もし高頻度で更新されるカウンターや、単一のホットなレコードを更新し続ける設計をしているなら、それは設計上の敗北だ。

悪い設計例:1つのレコードを激しく更新する

— 頻繁に更新される「残高」テーブル
UPDATE Accounts SET Balance = Balance – 100 WHERE AccountId = ‘hot_user_01’;

数千の並列スレッドがこの1行を狙う。コミットの瞬間に衝突が起き、9割がリトライの海に沈むだろう。

3. スループットを最大化する「攻め」の設計パターン

では、どう設計すべきか。実務レベルで推奨する戦略を3つ提示する。

A. 書き込みの分散(シャーディング)

カウンターであれば、1つのレコードを更新するのではなく、N個のバケットに分散させる。

— 1つのカウントを更新するのではなく、10個のバケットに分散して更新
UPDATE CounterTable SET Value = Value + 1
WHERE CounterId = ‘click_count’ AND ShardId = MOD(RANDOM(), 10);

読み取り時は `SUM()` で合算する。これで競合確率は劇的に下がる。

B. トランザクションの短縮(クリティカルパスの極小化)

トランザクション内で重い計算や外部API呼び出しを挟むのは愚策だ。

  • NG: 「外部計算 → 読み込み → 変換 → 書き込み」を1つのトランザクションで行う。
  • OK: 「計算」を事前に済ませ、トランザクション内は「読み込み → 書き込み」の最短経路のみにする。

C. 指数バックオフ付きリトライの実装

クライアントライブラリは自動リトライを行うが、アプリケーションコード側でも制御が必要だ。

// Goでのリトライ戦略のイメージ
// Abortされた際は、即座に再試行するのではなく、指数バックオフを入れる
for {
err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn spanner.ReadWriteTransaction) error {
// ロジック実行
})
if spanner.ErrCode(err) == codes.Aborted {
// ここでランダムなジッターを加えたバックオフを挟む
time.Sleep(calculateBackoff())
continue
}
return err
}

4. パフォーマンス上の注意点:読み取り専用トランザクションの活用

「楽観的並行性制御」が最も力を発揮するのは、「読み取り専用トランザクション」においてだ。

もし、あなたが更新を伴わない処理で `ReadWriteTransaction` を使っているなら、今すぐ修正してほしい。`SingleUseTransaction` や `ReadOnlyTransaction` を使えば、Spannerはタイムスタンプに基づいて「過去のある時点」のデータを読み取ることができる。

これにより、書き込みトランザクションとの競合がゼロになり、スループットは理論上の限界まで引き上げられる。

最後に:アーキテクトとしての提言

Cloud Spannerは、データベースの限界を定義し直すツールだ。しかし、そのポテンシャルを使いこなせるかどうかは、開発者が「分散環境におけるデータの競合」をどれだけ直感的に理解しているかにかかっている。

「ロックを奪い合うな、衝突を避けろ。どうしても衝突するなら、リトライをエレガントに処理せよ。」

この原則を守れば、あなたのシステムは数百万のユーザーが同時にアクセスしても、涼しい顔をしてトランザクションをさばき続けるはずだ。コードレビューの際、トランザクションの範囲が肥大化していないか、常に目を光らせてほしい。

健闘を祈る。

コメント

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