---
title: メッセージセマンティクス
url: https://doc.liz6.com/ja/distributed-systems/07-messages-and-streams/01-message-semantics
locale: ja
area: distributed-systems
tags:
- distributed-systems
- messages-and-streams
date: 2026-06-30
modified: 2026-07-16
description: メッセージは消失したり重複したりする可能性がある——信頼性の低いネットワーク上で信頼性の高い伝達を実現するには、3つのセマンティクスが3つのエンジニアリング上のトレードオフを表す。at-least-once は実装が最も容易だが、消費側で冪等性が求められる。exactly-once は、プロデューサの冪等性、トランザクション、コンシューマの連携が必要で、本質的にはメッセージングシステム上に分散トランザクションを組み立てている。
---

# メッセージセマンティクス

> メッセージは消失したり重複したりする可能性がある——信頼性の低いネットワーク上で信頼性の高い伝達を実現するには、3つのセマンティクスが3つのエンジニアリング上のトレードオフを表す。at-least-once は実装が最も容易だが、消費側で冪等性が求められる。exactly-once は、プロデューサの冪等性、トランザクション、コンシューマの連携が必要で、本質的にはメッセージングシステム上に分散トランザクションを組み立てている。

メッセージシステム（Kafka, RabbitMQ, NATS）におけるセマンティック保証は、分散システムの一貫性の特殊ケースである——同じ理論的基盤を持ちながら、異なる現れ方をする。

## 3つのセマンティクス

### At-Most-Once

```
Producer: メッセージ送信 → ACK を待たない → リトライしない
Consumer: メッセージ受信 → 処理 → ack (処理前または処理後)

メッセージが消失する可能性がある: producer が ack を受信できないが、broker は既に受信済み → producer は再送しない
               consumer が処理前に ack → クラッシュ → メッセージはキューから削除済み
```

使用例: 損失許容のシナリオ（メトリクス、ログ、重要でない通知）。

### At-Least-Once

```
Producer: メッセージ送信 → ACK を待つ → タイムアウト？ → リトライ
Consumer: 処理 → ack → もし ack 前にクラッシュ → メッセージは再配信される (別の consumer に再送)

メッセージが重複する可能性がある: producer がリトライ → broker が 2 通受信
               consumer が処理完了したが ack が消失 → broker が再送 → consumer が 2 通受信
```

使用例: 大半のメッセージシステム（Kafka, RabbitMQ の confirm モード）。代償として、**消費者は重複メッセージを処理できなければならない**。

### Exactly-Once

完全に非同期な分散システムにおいて厳密に保証することは理論的に不可能である（FLP の帰結）。実際には、以下の2つのアプローチで近づく。

1. **冪等プロデューサ (Kafka)**: プロデューサは各メッセージに一意な `(producer_id, sequence_number)` を付与する。broker は `sequence_number` をチェックし、重複であれば破棄する。保証：**同一プロデューサから同一パーティションへのメッセージは重複しない**。

2. **トランザクション書き込み**: プロデューサの複数のメッセージが1つのトランザクション内でアトミックに書き込まれる（コミット → 全メッセージが可視化、アボート → 何も可視化されない）。Kafka の `transactional.id` メカニズムがこれを実現する。

重要なのは、broker が重複排除とアトミックな可視性を保証できることだが、**コンシューマ側の exactly-once セマンティクスを実現するには、コンシューマ自身が冪等でなければならない**——つまり、コンシューマは `ORDER_STATUS = CONFIRMED` のような非冪等な更新を使ってはならない。

## 冪等コンシューマ

```
非冪等: UPDATE inventory SET count = count + 1 WHERE product_id = 42
        → リトライ → count が2回インクリメント → エラー

冪等:   INSERT INTO orders (order_id, user_id, product_id, status) VALUES (123, 456, 42, 'CREATED')
        → リトライ (同じ order_id=123) → UNIQUE 制約 → 無視または既存を保持 → 正解

または: UPDATE inventory SET count = ?, version = version + 1 WHERE product_id = 42 AND version = ?
        → 楽観的ロック → 最初のリトライは成功、後続の version mismatch → 「既に処理済み」
```

## Kafka 冪等プロデューサの詳細

```
Producer が broker に対して初回登録: producer_id を取得 (broker によって割り当てられ、グローバルにインクリメント)

各メッセージ: (producer_id, producer_epoch, base_sequence, [messages])

Broker:
  各プロデューサの最終成功 sequence_number を維持
  新しいメッセージを受信: sequence > last_seen + 1 なら "ギャップ" と判定
              sequence <= last_seen なら 重複 → 破棄 (ただし OK として記録)
              else → ログに追加
```

## 参考

- **Kafka idempotent producer**: kafka.apache.org/documentation/#semantics
- **Kafka transactions**: kafka.apache.org/documentation/#transactions

*Keywords: at-most-once, at-least-once, exactly-once, idempotent producer, transactional outbox, sequence number, deduplication*
