← Назад к вопросам
Как реализовать гарантию доставки ровно один раз?
2.4 Senior🔥 141 комментариев
#Брокеры сообщений
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как реализовать гарантию доставки ровно один раз
Это одна из самых сложных задач в распределённых системах. "Ровно один раз" = exactly-once.
Три типа гарантий доставки
1. At-most-once (максимум один раз)
- Сообщение может быть потеряно
- Пример: UDP
2. At-least-once (как минимум один раз)
- Сообщение дойдёт, но может быть дублировано
- Требует идемпотентности
- Пример: RabbitMQ, Kafka
3. Exactly-once (ровно один раз) ← Самая сложная!
- Дойдёт ровно один раз
Решение 1: Идемпотентные операции + дедупликация
class PaymentService:
def process_payment(self, order_id: str, amount: float, payment_id: str):
# payment_id — уникальный ID платежа
existing = self.db.query(Payment).filter_by(
payment_id=payment_id
).first()
if existing:
return existing # Идемпотентно возвращаем
payment = Payment(
id=payment_id,
order_id=order_id,
amount=amount,
status='completed'
)
self.db.add(payment)
self.db.commit()
return payment
# Клиент отправляет с идентификатором
payment_id = str(uuid.uuid4())
# Первый вызов
result1 = client.process_payment('order-123', 100.0, payment_id)
# Повтор (сеть упала)
result2 = client.process_payment('order-123', 100.0, payment_id)
# Обе операции идемпотентны
assert result1.id == result2.id
Решение 2: Outbox Pattern (самое практичное!)
class OrderService:
def create_order(self, user_id: int, items: list):
try:
order = Order(
id=str(uuid.uuid4()),
user_id=user_id,
items=items
)
self.db.add(order)
# Записываем событие в outbox в ОДНОЙ транзакции
outbox_event = OutboxEvent(
id=str(uuid.uuid4()),
order_id=order.id,
event_type='order.created',
payload={
'order_id': order.id,
'user_id': user_id,
'items': items
},
status='pending'
)
self.db.add(outbox_event)
self.db.commit() # Одна транзакция!
return order
except Exception:
self.db.rollback()
raise
# Отдельный процесс читает outbox и отправляет события
class OutboxProcessor:
def process(self):
events = self.db.query(OutboxEvent).filter_by(
status='pending'
).limit(100).all()
for event in events:
try:
self.event_bus.publish(event.event_type, event.payload)
event.status = 'sent'
self.db.commit()
except Exception:
logger.error(f"Failed to send event {event.id}")
Решение 3: Kafka с транзакциями
from kafka import KafkaProducer
class KafkaExactlyOnce:
def __init__(self):
self.producer = KafkaProducer(
bootstrap_servers=['localhost:9092'],
transactional_id='my-producer' # Ключ для exactly-once
)
def process_order_exactly_once(self, order_data):
with self.producer.transaction():
# В одной транзакции Kafka:
result = self.process_order(order_data)
self.producer.send('order-processed', value=result)
# Откатывается или коммитится атомарно
Решение 4: Saga Pattern (микросервисы)
class OrderSaga:
def create_order(self, order_id: str, items):
try:
# Шаг 1: Зарезервировать товар
self.inventory_service.reserve(items)
# Шаг 2: Обработать платёж
self.payment_service.charge(order_id)
# Шаг 3: Отправить заказ
self.shipping_service.ship(order_id)
except Exception:
# Компенсирующие транзакции
self.inventory_service.release(items)
self.payment_service.refund(order_id)
raise
Решение 5: REST API с идемпотентностью
from fastapi import FastAPI, Header, HTTPException
app = FastAPI()
@app.post("/orders")
def create_order(
order_data: OrderCreate,
idempotency_key: str = Header(None)
):
# Клиент отправляет: Idempotency-Key: uuid-123
if not idempotency_key:
raise HTTPException(status_code=400)
# Проверяем: был ли уже такой заказ?
existing = db.query(Order).filter_by(
idempotency_key=idempotency_key
).first()
if existing:
return existing # Идемпотентно
order = Order(
id=str(uuid.uuid4()),
idempotency_key=idempotency_key,
**order_data.dict()
)
db.add(order)
db.commit()
return order
Когда каждый подход
Идемпотентные операции:
- Простые операции
- REST API
- Платежи
Outbox Pattern:
- Микросервисы
- Event sourcing
- Message-driven
Kafka транзакции:
- Потоковые обработки
- Real-time системы
Saga Pattern:
- Распределённые транзакции
- Долгоживущие процессы
Ключевые факты
- Exactly-once в распределённых системах очень сложно
- На практике: idempotency key + at-least-once = exactly-once эффект
- Outbox pattern — золотой стандарт
- Kafka генерирует unique offset для каждого сообщения
- Идемпотентность = ключ к exactly-once