正確に1回vs少なくとも1回
1)セマンティクスを議論する理由
配信セマンティクスは、クラッシュやリトレース時に受信者がメッセージを表示する頻度を決定します:- At-most-once-反復なしで、損失は可能です(まれに許容できます)。
- 少なくとも1回-失うことはありませんが、重複は可能です(ほとんどのブローカー/キューのデフォルト)。
- 正確に一度-各メッセージは、観察された効果の観点から正確に1回処理されます。
重要な真実:グローバルなトランザクションと同期的な整合性のない分散型の世界では、「クリーン」なエンド・ツー・エンドを正確に1回は達成できません。私たちは正確に1回だけ効果的に構築します:輸送の繰り返しを許可しますが、観察された効果が「1回のように」なるように処理を独特にします。
2)故障パターンと重複が発生する場所
リプレイが原因で表示されます:- 損失ack/commit(プロデューサー/ブローカー/コンサマーは「聞こえませんでした」確認)。
- リーダー/レプリカの再選、ネットワーク破損後の回復。
- 任意のエリア(kliyent→broker→konsyumer→sink)でタイムアウト/リトリート。
結果:輸送の「配達の独自性」に頼ることはできません。エフェクトの管理:データベースへの書き込み、お金の引き落とし、手紙の送信など。
3)プロバイダーとそれが本当に何であるかで正確に一度
3.1カフカ
レンガを与える:- Idempotent Producer ('enable。idempotence=true')-引き込み時にプロデューサー側の重複を防ぎます。
- トランザクション-複数のバッチでメッセージをアトミックにパブリッシュし、消費オフセットをコミットします(「ギャップ」のない読み取りプロセス書き込みパターン)。
- 「圧縮」(Compaction)-最後の値をキーで格納します。
しかし「、チェーンの終わり」(シンク:DB/payment/mail)はまだidempotencyを必要とします。そうでなければ、ハンドラのダブルはエフェクトダブルを引き起こします。
3.2 NATS/ウサギ/SQS
デフォルトはack/redeliveryで最低1回です。正確に一度は、アプリケーションレベルで達成されます:キー、デッドストア、アップサート。
結論:正確に一度の輸送≠正確に一度の効果。後者はハンドラで行われます。
4)効果的に正確に一度以上を構築する方法
4.1 Idempotencyキー
各コマンド/イベントには、'payment_id'、 'order_id#step'、 'saga_id#n'という自然なキーがあります。ハンドラ:- チェック「すでに見た?」-TTL/RetschのDedup-stor (Redis/DB)。
- あなたが見た場合、以前に計算された結果を繰り返すか、no-opを行います。
lua
-- SET key if not exists; expires in 24h local ok = redis.call("SET", KEYS[1], ARGV[1], "NX", "EX", 86400)
if ok then return "PROCESS" else return "SKIP" end
4.2ベースのアップサート(idempotent cink)
エントリーはUPSERT/ON CONFLICTを介してバージョン/金額チェックを行います。
PostgreSQL:sql
INSERT INTO payments(id, status, amount, updated_at)
VALUES ($1, $2, $3, now())
ON CONFLICT (id) DO UPDATE
SET status = EXCLUDED.status,
updated_at = now()
WHERE payments.status <> EXCLUDED.status;
4.3トランザクションアウトボックス/受信トレイ
Outbox:ビジネストランザクションとイベントツーパブリッシュエントリが同じデータベーストランザクションで発生します。バックグラウンドパブリッシャーはアウトボックスを読み取り、ブローカーに送信します→状態とイベントの間に矛盾はありません。
受信トレイ:着信コマンドの場合、'message_id'と実行前の結果を保存します。再処理はレコードを見、副作用を繰り返しません。
4.4一貫したチェーン処理(読み取り→プロセス→書き込み)
Kafka:トランザクション「オフセットを読む→→コミットの結果を書き留めた」1つの原子ブロック。
トランザクションなし:「最初に結果/受信トレイを書き留め、次にack」;クラッシュすると、重複すると受信トレイが表示され、no-opで終了します。
4.5 SAGA/オフセット
idempotencyが不可能な場合(外部プロバイダがお金を返済した場合)、補償操作(払い戻し/無効)とidempotent外部API(同じ「Idempotency-Key」で「POST」を繰り返すと同じ結果が得られます)を使用します。
5)少なくとも一度は十分であるとき
キーベースの圧縮によるキャッシュ/実体化ビューの更新。
再インクリメントが許容されるカウンタ/メトリック(またはバージョンのデルタを格納)。
セカンダリ文字が重要でない通知(とにかくキーを置くことをお勧めします)。
ルール:ダブルがビジネスの意味を変更しない場合、または簡単に→少なくとも一度+部分的な保護を見つけることができます。
6)性能および費用
正確に一度(「効果的」でも)より多くの費用がかかります:追加のレコード(受信トレイ/送信トレイ)、キーの保存、トランザクション、診断はより困難です。
少なくとも1回はより安く/簡単で、スループット/p99で優れています。
評価:保護の二重対価の二重×確率の価格。
7)サンプル構成とコード
7.1カフカプロデューサー(idempotence+transactions)
properties enable.idempotence=true acks=all retries=INT_MAX max.in.flight.requests.per.connection=5 transactional.id=orders-writer-1
java producer.initTransactions();
producer.beginTransaction();
producer.send(recordA);
producer.send(recordB);
// также можно atomically commit consumer offsets producer.commitTransaction();
7.2受信トレイ・コンソール(擬似コード)
pseudo if (inbox.exists(msg.id)) return inbox.result(msg.id)
begin tx if!inbox.insert(msg.id) then return inbox.result(msg.id)
result = handle(msg)
sink.upsert(result) # идемпотентный синк inbox.set_result(msg.id, result)
commit ack(msg)
7.3 HTTP Idempotency-Key(外部API)
POST /payments
Idempotency-Key: 7f1c-42-...
Body: { "payment_id": "p-123", "amount": 10.00 }
同じキー→同じ結果/ステータスでPOSTを繰り返しました。
8)観測可能性と指標
'duplicate_attempts_total'-ダブルがキャッチされた回数(受信トレイ/Redisによる)。
'idempotency_hit_rate'-繰り返しの割合がidempotencyによって「保存」されます。
'txn_abort_rate' (Kafka/DB)-ロールバックのシェア。
'outbox_backlog'-公開遅延。
'exactly_once_path_latency {p95、 p99}'と'at_lest_once_path_latency'-オーバーヘッド。
監査ログ:'message_id'、 'idempotency_key'、 'saga_id'、 'attempt'の束。
9)テストプレイブック(ゲーム日)
リプレイを送信:生産者は人工タイムアウトでリトレイします。
「sink and ack」間のクラッシュ:受信トレイ/Upsertがダブルを防ぐことを確認します。
再配達:ブローカーの再配達を増加させて下さい;デッドアップを確認しろ。
外部APIのIdempotency:同じキーで繰り返されるPOSTは同じ答えです。
Lead Change/Network Break: Kafka Transactions/Consumers Behaviorをチェックします。
10)アンチパターン
トランスポートに依存します:「我々は正確に一度でKafkaを持っているので、あなたはキーなしですることができます」-いいえ。
録音前のNo-op ack: ackledがシンクがドロップ→ロス。
DLQ/ジッタ後退の欠如:無限のリプレイと嵐。
自然キーの代わりにランダムなUUID:重複除外するものはありません。
Inbox/Outboxをインデックスなしの生産テーブルと混合:ホットロックとp99テール。
外部プロバイダでのAPIを使用せずに取引を行うことができます。
11)選択チェックリスト
1.二重価格(お金/法的/UX)と保護価格(遅延/複雑さ/コスト)。
2.自然なイベント/操作キーはありますか?そうでない場合は、安定したものを思い付く。
3.シンクはアップサート/バージョン管理をサポートしていますか?それ以外の場合-受信トレイ+補償。
4.グローバルトランザクションは必要ですか?そうでない場合は、SAGAにセグメント。
5.リプレイ/長期保存が必要ですか?カフカ+アウトボックス。高速RPC/低遅延が必要ですか?NATS+Idempotency-Key。
6.マルチテナンシーとクォータ:キー/スペースの分離。
7.Observability: idempotencyとbacklogのメトリックが含まれています。
12) FAQ
Q:正確に「数学的」なエンドツーエンドを実現することは可能ですか?
A: 1つの一貫した店およびトランザクションの狭いシナリオでだけずっと。一般的な場合は、いいえ。idempotencyによって効果的に一度使用して下さい。
Q:どちらが速いですか?
A:少なくとも一度。正確に一度、トランザクション/キーストレージ→p99以上とコストを追加します。
Q: idempotenceキーをどこに保存しますか?
A: TTLまたは受信トレイ(PK=message_id)のクイックストップ(Redis)。支払いの場合-より長い(日/週)。
Q: TTLのdedupキーを選ぶ方法か?
A:最小=最大再配達時間+運用マージン(通常24〜72時間)。財務のために-もっと。
Q: Kafkaでキーによる圧縮がある場合、キーは必要ですか?
A:はい。Compactionはストレージを削減しますが、同期をidempotentにすることはありません。
13)合計
少なくとも一度-基本的で信頼性の高いトランスポートセマンティクス。
Idempotency-Key、 Inbox/Outbox、 Upsert/versions、 SAGA/compensationといったビジネスエフェクトがプロセッサレベルで実現されます。
選択は、コストの妥協↔重複のリスク↔操作の容易さです。自然なキーを設計し、打撲を偶然のものにし、観測性を追加し、定期的にゲームをプレイします。その後、パイプラインはレトラや故障の嵐でも予測可能で安全です。