【ITニュース解説】Mitigating DB Lock Contention and Race Conditions in Asynchronous Webhook Ingestion
2026年10月16日に「Dev.to」が公開したITニュース「Mitigating DB Lock Contention and Race Conditions in Asynchronous Webhook Ingestion」について初心者にもわかりやすく解説しています。
ITニュース概要
非同期Webhookの重複処理は「チェック→実行」だけだと、同時アクセスで重複や競合が発生する。確実に一度だけ処理するには、処理開始時の排他ロック、DBのユニーク制約、競合時の指数バックオフリトライを組み合わせる。これにより高負荷でも正確な処理を保証できる。
ITニュース解説
システムが他のシステムから特定のイベント通知(Webhook)を受け取る際、同じ通知が複数回送られてくる可能性がある。たとえば、ネットワークの一時的な問題や、送信側のシステムが応答を受け取れなかったと判断した場合などに、同じ内容のWebhookが再送される。このような状況で、システムが同じイベントを何度も処理してしまわないよう、「冪等性」(何度同じ操作を繰り返しても結果が変わらない性質)を保証することが重要になる。
しかし、この冪等性を実現するための一般的なアプローチ「Check-Then-Act」(まずデータベースでイベントIDが存在するか確認し、存在しなければ処理を進める)は、システムが高負荷状態になるとうまく機能しない場合がある。これは「競合状態」(Race Condition)と呼ばれる問題で、複数の処理が同時に実行されることで、予期せぬ結果が生じる。具体的には、2つの処理(スレッドAとスレッドB)がほぼ同時に同じイベントIDをデータベースに問い合わせ、どちらも「存在しない」という結果を受け取る。その後、どちらかの処理が先にデータベースに登録を試み、成功する。しかし、もう一方の処理も同じく登録を試みてしまい、結果として重複したデータが作成されたり、データベースの制約違反でエラーが発生したりする。これは「TOCTOU(Time-of-Check to Time-of-Use)」、つまり確認した時点と実際に使用する時点との間にずれがあるために発生する。特に、上流のシステムからのリトライが集中するような状況では、この問題が顕著になり、期待される重複排除が機能せず、複数の処理が同時に実行されてしまう。
このような問題を解決し、高い信頼性で冪等性を保証するためには、より堅牢なアーキテクチャが必要となる。その中心にあるのが「決定論的冪等性」という考え方で、これは三段階の防御モデルによって実現される。まず、データベースへの書き込み操作が開始される直前に排他ロックをかけることで、複数の処理が同時に書き込み領域に入ることを防ぐ。次に、データベース自体にユニーク制約を設定することで、たとえロックが一時的に機能しなかったとしても、重複したデータの登録を物理的に阻止する。最後に、ロックによって発生する一時的な競合(ロック競合)に対しては、指数バックオフとジッターを組み合わせた再試行メカニズムを導入し、システムの可用性を損なわずに処理の完了を待つ。
最初の防御策である「即時排他ロック(BEGIN IMMEDIATE)」は、SQLiteのようなリレーショナルデータベースで特に有効である。通常のトランザクション(BEGIN DEFERRED)では、ロックは最初の書き込み操作が実行されるまで取得されない。この遅延の間に、他の処理が共有読み取りを開始し、その後に書き込みロックへのアップグレードを試みた際にデッドロックが発生する可能性がある。しかし、「BEGIN IMMEDIATE」を明示的に指定することで、トランザクション開始時に即座に予約ロック(RESERVEDロック)が取得される。これにより、他の書き込み意図を持つトランザクションが同時に重要な領域に入るのが防がれ、ロックアップグレードによるデッドロックが排除される。
二番目の防御策は「データベースレベルのユニーク制約」である。データの整合性をアプリケーションのロジックだけに頼るべきではない。データベースにPRIMARY KEYやUNIQUEインデックスをイベントIDフィールドに設定することで、重複した書き込みをデータベースエンジン自身が自動的に拒否する。これにより、もしアプリケーションのチェックが何らかの理由で迂回されたとしても、データベースが最終的な防衛線として機能し、重複データの挿入を「IntegrityError」として拒否する。このエラーを受け取ったアプリケーションは、安全にトランザクションをロールバックし、そのイベントをすでに処理済みとして扱うことができる。
三番目の防御策は「指数バックオフとジッターによるロック競合への対処」である。排他ロックを積極的に利用すると、システムが高負荷になった際に「OperationalError: database is locked」といったロック競合が頻繁に発生する可能性がある。このような場合、即座にリクエストを拒否するとシステムの可用性が低下してしまう。そこで、短時間のリトライループに、ミリ秒単位の指数バックオフ(再試行の間隔を指数関数的に長くしていく)とジッター(ランダムな遅延を加える)を組み合わせることで、一時的なロック競合を効果的に解決しながら、高いシステムスループットを維持する。ジッターは、多くの処理が同時に再試行して再びロック競合を引き起こす「サンダーストーム」現象を防ぐ役割も果たす。
これらの防御策を検証するために、Pythonで実装されたシミュレーションコードがある。これは、20の異なるスレッドが同じWebhookペイロードを同時に処理しようとする状況を再現する。SQLiteのインメモリデータベースを共有キャッシュモードで利用し、複数のスレッドが同一のデータベースにアクセスする環境を作ることで、実際の競合状態を模擬する。このコードのprocess_webhook_idempotent関数は、BEGIN IMMEDIATEによる排他ロック、イベントIDの存在チェック、そしてINSERTによるデータ永続化という一連の処理を、データベースのユニーク制約と、ロック競合時の指数バックオフ・リトライと組み合わせている。
このシミュレーションを実行すると、20の同時リクエストに対して、期待通りに正確に1つのリクエストだけが成功し、残りの19リクエストは重複として適切にブロックされる。この結果は、上記の二重防御メカニズムが効果的に機能していることを示している。具体的には、一部のスレッドはロックを取得した後にアプリケーションレベルのチェックで「すでに処理済み」と判断され、残りのスレッドはロックウィンドウに入り、データベースレベルのユニーク制約によって「制約違反」として拒否される。この検証により、指数バックオフとジッターの組み合わせが、高負荷下でもデッドロックや未処理のエラーを発生させずに、システム全体の可用性を維持できることが確認される。
分散システム設計において、Webhookのようなイベントは「少なくとも一回配送」が保証されているのが一般的であるため、レシーバー側で冪等性を考慮することは必須である。安易なCheck-Then-Actの実装に頼るのではなく、データベースへの書き込み時には早期かつ明示的にロックを取得すること、データベースレベルのユニーク制約を最終防衛線として必ず設定すること、そしてロック競合に対する回復策として指数バックオフを適用することが、高負荷な本番環境においてもレジリエントで予測可能な、完全に冪等なWebhook取り込みパイプラインを構築するための重要な教訓となる。