プレイブック

Webhook でのオプティミスティック同時実行性 (Webhook 2)

Webhook による同期応答が同時に同じ支払いに触れた場合、バージョン トークンとリースは競合をどのように解決しますか?端末支払いでのクライアント シークレットの読み取りが古い…

分散型決済エンジン

一部 17 の 22

取得と完了の間のギャップを埋める一連の分散型支払いアーキテクチャ。

Distributed payment engine architecture diagram

前のセクションでは、結果整合性が避けられない理由を説明しました。PSP はトランザクション プロトコルに参加できません。サガと調整が本当の答えです。このセクションでは、不一致ウィンドウのピークの瞬間、つまり Webhook と同期応答が同じ支払いレコードに同時にアクセスする瞬間に焦点を当てます。

チェックアウト オーケストレーターは請求リクエストを送信します。プロバイダー ゲートウェイは PSP からの応答を受信します。同時に、場合によっては数ミリ秒早く、場合によっては遅くなりますが、同じ支払いに対して Webhook が到着します。どちらのパスも正確な情報を運ぶことができます。両方が同時に書き込みを試行すると、結果は更新が失われるか、さらに悪いことに、端末の支払いでの古い読み取りに基づく誤ったクライアント応答になります。```text Senkron yanıt ──► Payment #42 (version=3) ──► Captured Webhook ──► Payment #42 (version=3) ──► Captured (tekrar?) │ ▼ Version token + lease → tek kazanan yazar


## 最初に説明した概念```text
📦 Version Token (Optimistic Lock)
Her güncellemede artan sayaç; yazma yalnızca beklenen sürüm eşleşirse başarılı olur.

📦 Lease
Bir worker'ın belirli bir ödeme kaydını işleme hakkını süre sınırlı olarak alması.

📦 Stale Read
İşlem sırasında okunan ama yazma anında artık geçerli olmayan eski sürüm.

📦 Terminal Payment
Captured, Failed veya Refunded gibi geri dönüşü olmayan nihai statü.
```リースとは、労働者が「このレコードを処理しています」と言うときです。バージョン トークンは、「このバージョンがまだ表示されている」ことを意味します。これらを組み合わせることで、Webhook と同期パスが相互に上書きされるのを防ぎます。

## 2 つの道、1 つの記録: レースの始まり

リダイレクトベースの支払いでは、同期パスは通常 `Pending` を返します。実際の結果は Webhook に付属しています。カード支払いの場合、両方のルートで `Captured` または `Failed` を運ぶことができ、両方がほぼ同時に到着する可能性があります。チェックアウト オーケストレーターは、両方のパスから同じ支払明細行に情報を書き込もうとします。

典型的なエラー: 両方のハンドラーがレコードを読み取り、ステータスを更新し、保存します。最後に書いた人が勝ちです。その間の更新は静かに消えます。さらに危険なシナリオ: オーケストレーターはターミナル ステータスに切り替える前に古いバージョンを読み取り、まだ有効であるように見えるクライアント シークレットまたはリダイレクト URL をクライアントに返します。実際には、支払いはすでに完了しているか失敗しています。```text
T=0  Orchestrator: charge gönder
T=1  Webhook gelir → Captured yaz (version 2→3)
T=2  Senkron yanıt gelir → Pending okudu (version 1)
     → client'a redirectUrl döner (bayat!)
T=3  Müşteri redirect'e gider → ödeme zaten Captured
```## バージョントークン: バージョンが一致する場合のみ書き込みます

単調増加する `version` フィールドが各支払いレコードに保持されます。更新は、`UPDATE ... WHERE id = ? AND version = ?` という条件で行われます。一致するものがない場合、更新はゼロ行に影響します。これは、別のパスが干渉していることを示します。```text
Webhook handler
  READ payment (version=2, status=Processing)
  → status=Captured, version=3
  UPDATE WHERE version=2 ✓ (1 row)

Senkron handler (bayat okuma)
  READ payment (version=2, status=Processing)  ← webhook henüz commit olmadı
  → status=Captured, version=3
  UPDATE WHERE version=2 ✗ (0 rows — webhook zaten yazdı)
  → yeniden oku, terminal statüyü gör, client secret döndürme
```バージョン トークンだけでは十分ではありません。また、古い読み取りを検出した後に何をするかを定義する必要があります。つまり、再度読み取り、端末ステータスを確認し、現在のステータスのみをクライアントに返します。

## リース: 限られた期間、Webhook 処理の権利を取得します。

Webhook ハンドラーは、「支払い #42 を 30 秒間処理しています」というレコードにアクセスする前に短いリースを取得します。リース期間中、別のワーカーは Webhook またはリカバリ ストリーム内の同じレコードを処理できません。```text
Webhook gelir
  → lease al (paymentId, ttl=30s)
  → lease alınamazsa → defer / retry
  → lease alındı → version token ile güncelle
  → lease bırak
```リースにより、同じ Webhook が 2 人のワーカーによって同時に処理されることが防止されます。バージョン トークンは、異なるパス (同期と Webhook) 間の競合を解決します。 2 人は異なる問題に答えます。これらは一緒に使用する必要があります。

## 端末支払いにおけるクライアント シークレットの漏洩

最も深刻な古い読み取りシナリオは、ターミナル ステータスでクライアント シークレットまたはリダイレクト URL を返すことです。支払いが `Captured` になった後も `Pending` + redirectUrl を顧客に返すと、不要な 2 回目の請求が試行されたり、混乱が生じたりすることになります。

ルールは単純です。ターミナル ステータスに移行した支払いに対して、クライアント シークレット、リダイレクト URL、または再試行トークンは決して返されません。ハンドラーは、バージョンの競合を受け取った場合、または古い読み取りが疑われる場合にレコードを再読み取りします。終了ステータスを確認した場合は、最終結果のみを返します。

|ステータス |クライアントに戻る |
| --- | --- |
|処理中、リダイレクトが必要です | redirectUrl (有効) |
|捕獲(端末) |成功の結果、秘密はありません |
|失敗しました (端末) |エラー結果、秘密なし |
|バージョンの競合 → 再読み込み → キャプチャ |成功の結果、秘密はありません |

## 混同されやすい区別```text
❌ Pessimistic lock her zaman daha güvenlidir
✓ Ödeme akışında kısa süreli lease + version token, throughput'u korurken yarışı çözer

❌ Version conflict = hata, exception fırlat
✓ Version conflict = başka yol kazandı; yeniden oku ve güncel duruma uy

❌ Lease ve version token aynı şeyi yapar
✓ Lease aynı kaydın eşzamanlı işlenmesini engeller; version token eşzamanlı yazmayı
```## リースとバージョンのトークン

|基準 |リース |バージョントークン |
| --- | --- | --- |
|ブロックされました |同じレコードの並列処理 |更新が失われました |
|期間 | TTL限定 |永続的、書き込みごとに増加 |
|競合行為 |待つ/延期する |再読み取り/再試行 |

## オプティミスティック同時実行チェックリスト

1. 支払いの更新は `WHERE version = ?` 条件で行われていますか?
2. バージョンの競合を受信した場合、ハンドラーは端末のステータスを再読み込みしてチェックしますか?
3. ターミナル ステータスでクライアント シークレットまたはリダイレクト URL を返すことは、コード レベルでブロックされていますか?
4. Webhook ハンドラーは操作を開始する前にリースされていますか?
5. リース期間は Webhook 処理時間の P99 より長いですか?
6. 同期応答ハンドラーと Webhook ハンドラーは同じ終了ロジックを共有しますか?

## この記事で覚えておくべきこと

1. Webhook を使用した同期パスは同じレコードに同時にアクセスします。楽観的な同時実行が、この競争に対する標準的な答えです。
2. バージョン トークンが失われると更新ができなくなります。リースにより、同じレコードの並列処理が防止されます。
3. バージョンの競合はエラーではなく、再読み込みの信号です。
4. 端末の支払いで古い情報を読み取らずにクライアント シークレットを返すことは、セキュリティと UX に重大な間違いを引き起こします。

> レースを解決するために悲観的なロックは必要ありません。必要なのは、古い読み取りがクライアントに到達するのを防ぐ、バージョン トークンとリースの規律ある組み合わせです。

次のセクションでは、これらのレースを確認し、支払い ID との相関関係、ステップバイステップのイベント ログ、および遅延ファイナライズ メトリクスの最終ステップを確認するための可観測性に進みます。

FAQ

よくある質問

バージョントークン(オプティミスティックロック)とは何ですか?

更新ごとにカウンタが増加します。書き込みは、期待されるバージョンが一致する場合にのみ成功します。

リースとは何ですか?

労働者が特定の支払記録を処理する期限付きの権利。

「悲観的なロックは常に安全である」は本当ですか?

支払いフロー内の短いリース + バージョン トークンにより、スループットを維持しながら競合を解決します

このセクションでは何を修正しますか?

ここでの楽観的な同時実行性はパフォーマンスの最適化ではありません。ターミナルステータスで不正なデータを返さない仕組みです。 Webhook を使用すると、同期パスは同じレコードに同時にアクセスします。楽観的な同時実行が、この競争に対する標準的な答えです。前のセクションでは、結果整合性が避けられない理由を説明しました。PSP はトランザクション プロトコルに参加できません。サガと調整が本当の答えです。このセクションでは、不一致ウィンドウのピークの瞬間、つまり Webhook と同期応答が同じ支払いレコードに同時にアクセスする瞬間に焦点を当てます。

学んだエンジニアリング原則

  • バージョン トークンが失われると更新ができなくなります。衝突は再読信号です。
  • リースとバージョン トークンはさまざまな人種を解決します。両方を一緒に使用する必要があります。
  • 端末支払いでは、クライアント シークレットを古いものとして読み取らずに返す必要はありません。

続きを読む

続きを読む

シリーズの次のシリーズ

エッセイ

支払いの観察可能性と相関性

各ログ、メトリクス、トレースを支払い ID と関連付けるにはどうすればよいですか?ステップバイステップのイベントログと遅延ファイナライズメトリクスはどのように操作を節約しますか?

シリーズの次のシリーズ

同じシリーズ

Paylaş