【入門編】 トランザクションリトライロジック – Cloud Spanner

こんにちは!Cloud Spannerの世界へようこそ。
私はこれまで数多くの分散データベースシステムを見てきましたが、Cloud Spannerほど「美しく、かつ強力なシステム」はありません。

「全世界規模でデータをガッチリ整合性を保ったまま保存できる」という夢のようなデータベースなのですが、初心者の人が最初に直面して「おや?」とつまずくポイントが一つだけあります。それが今回解説する「トランザクションリトライロジック」です。

難しそうな名前がついていますが、安心してください。ここをクリアすれば、あなたも立派なCloud Spanner使いの仲間入りです。一緒に優しく紐解いていきましょう!

—

1. まずはイメージしよう:人気ラーメン店での「注文のバッティング」

Cloud Spannerのリトライ戦略を理解するために、少し日常の例え話をさせてください。

あなたは今、大人気のラーメン店にいます。このお店の食券機は非常にハイテクで、世界中の支店とリアルタイムで在庫が連動しています。
今日限定の「幻の特製ラーメン」が、残り「1杯」だけになりました。

  • あなた:「これください!」(ボタンをポチッ)
  • 別のお客さん:「これください!」(ほぼ同時にポチッ)

さて、食券機はどう判断するでしょうか?
世界中のどこからアクセスがあっても、「絶対に正確に1杯しか売らない」のがCloud Spannerの仕事です。そのため、食券機は次のように判断します。

> 「おっと! ほぼ同時にボタンが押されたから、先着1名の処理が終わるまで、もう一人は少し待ってくれい!」

この「ちょっと待って、もう一回やり直してね」という仕組みが、Cloud Spannerにおけるトランザクションリトライの正体です。

—

2. なぜ「待たされる(競合が起きる)」のか?

Cloud Spannerは、世界中に散らばるサーバーがまるで1台の巨大なコンピュータであるかのように動く、驚異的なデータベースです。

複数のユーザーが、全く同じデータの書き換え(例えば、人気商品の在庫数や、銀行口座の残高)を「同時に」行おうとすると、データの矛盾を防ぐために、Spannerの内部で「順番待ち(ロックのような状態)」が発生します。

このとき、データベースの裏側では次のような会話がなされています。

1. 「同時並行で書き込みが来たぞ。データの整合性を守るために、順番をきっちり整理しよう」
2. 「こっちの処理を先に通すから、あっちの処理は一旦ストップさせて…よし、やり直させよう!」

これが、アプリケーション側から見ると「エラー(正確にはアボート:中断)」として返ってくる現象です。

初心者の方はここで「エラーが出た!データベースが壊れた!?」と焦ってしまうのですが、違うんです。これは「データの正確性を守るために、システムがわざと一旦止めて、仕切り直そうとしている」という、いわば健全な防衛反応なのです。

—

3. 解決の切り札:「指数バックオフ」という賢い作戦

では、エラー(中断)が起きたとき、アプリケーションはどう振る舞うべきでしょうか?
「エラーが出た!すぐにもう一回だ!」と、何千人ものユーザーが一斉にリトライしたらどうなるでしょう?

ラーメン店の例で言えば、店員が「少々お待ちください!」と言っているのに、目の前で「まだか!まだか!」と連打し続けるようなものです。これではお店(データベース)がパンクしてしまいますよね。

そこで登場するのが、「指数バックオフ(Exponential Backoff)」という賢いリトライ戦略です。

指数バックオフのルール

リトライするまでの「待ち時間」を、失敗するたびにネズミ算式(指数関数的)に増やしていく作戦です。

  • 1回目の失敗: すぐにリトライせず、「1秒」待つ
  • 2回目の失敗: 次は少し長めに、「2秒」待つ
  • 3回目の失敗: さらに長めに、「4秒」待つ
  • 4回目の失敗: 「8秒」待つ…

さらに、これに「ランダムな揺らぎ(ジッター)」を少し混ぜるのがプロの技です。みんなが一斉に「2秒後」「4秒後」に突撃するとまた渋滞(競合)が起きるので、時間を少しずつズラしてあげるのです。

—

4. コードで見てみよう(実装のイメージ)

百聞は一見にしかず。プログラムの世界では、このリトライ処理をどのように書くのか、Python風の擬似コードで見てみましょう。

import time
import random

def update_inventory_with_retry(spanner_client, item_id):
max_retries = 5
delay = 1.0 . # 初回の待ち時間は1秒

for attempt in range(max_retries):
try:
# — トランザクションの開始 —
with spanner_client.transaction() as transaction:
# 1. 現在の在庫を読む
current_stock = transaction.read_stock(item_id)

if current_stock <= 0: raise Exception("売り切れです!") # 2. 在庫を減らして書き込む transaction.update_stock(item_id, current_stock - 1) # トランザクションが成功したらループを抜ける print("購入成功!") return True except SpannerAbortedError: # 競合による中断が発生した場合(ここが肝!) if attempt == max_retries - 1: print("リトライ回数が上限に達しました。時間を置いて再度お試しください。") raise print(f"混み合っています。{delay}秒後にリトライします... (試行回数: {attempt + 1})") # 1. 指数関数的に待ち時間を増やす (1秒 -> 2秒 -> 4秒…)
# 2. ランダムな揺らぎ(ジッター)を加えて渋滞を防ぐ
actual_delay = delay (1.0 + random.random() 0.1)
time.sleep(actual_delay)

delay = 2 . # 次回は待ち時間を倍にする

return False

このコードの美しいポイント

1. 例外をキャッチしている: `SpannerAbortedError`(競合による中断)を優しく受け止めています。
2. 諦めずに優しく待つ: すぐに諦めず、少しずつ待ち時間を増やしながら最大5回まで挑戦します。
3. サーバーに優しい: ランダムな時間を混ぜることで、他のユーザーとの「衝突事故」を華麗に回避しています。

—

5. 先輩エンジニアからのアドバイス:ここをクリアすればバッチリ!

Cloud Spannerを扱う上で、この「競合とリトライ」は避けて通れません。しかし、それはSpannerの性能が低いからではなく、「世界中どこからでもデータの整合性を絶対に崩さない」という最強の盾を持っているからこその仕様です。

ここをクリアするためのポイントをまとめます。

  • エラーを恐れない: リトライ前提で作られているため、トランザクションの中断は「バグ」ではなく「仕様」です。
  • リトライライブラリを活用する: 多くの公式クライアントライブラリ(Java, Python, Goなど)には、この指数バックオフのリトライ機能が最初から標準で組み込まれています。自分でイチから書く必要すらないことも多いです。
  • 設計を見直す: もし特定のデータ(例えば、たった1行のカウンターなど)に世界中からのアクセスが集中しすぎてリトライが多発する場合は、キーの設計をシャード(分散)させるなどのテクニックを使います。

ここを理解できたあなたは、もうCloud Spannerの核心を捉えています。
怖がらずに、どんどん分散データベースの荒波に漕ぎ出していきましょう!応援しています!

コメント

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