Ответ
Семантика 'exactly-once' (доставка ровно один раз) — это гарантия того, что сообщение будет обработано потребителем ровно один раз, даже в условиях сбоев сети или повторных отправок.
На практике "чистая" exactly-once доставка на уровне сети почти невозможна. Поэтому она реализуется как комбинация двух других механизмов:
Exactly-Once = At-Least-Once + Идемпотентный потребитель
-
Доставка 'At-Least-Once' (как минимум один раз): Эту часть обеспечивает система очередей (например, Kafka или RabbitMQ с
publisher confirms). Она гарантирует, что сообщение не будет потеряно и будет доставлено потребителю хотя бы раз. -
Идемпотентный потребитель (Idempotent Consumer): Это ключевой элемент. Потребитель должен быть спроектирован так, чтобы повторная обработка одного и того же сообщения не приводила к побочным эффектам (например, повторному списанию денег).
Способы реализации идемпотентности
-
Уникальный идентификатор операции: Каждому сообщению или операции присваивается уникальный ID (
correlation_id,idempotency_key). Потребитель хранит ID уже обработанных сообщений (в БД или Redis) и игнорирует дубликаты. -
Транзакционная обработка: Бизнес-логика и сохранение ID обработанного сообщения выполняются в рамках одной атомарной транзакции.
Пример идемпотентного обработчика (Python)
import redis
# Используем Redis для хранения ID обработанных сообщений
processed_ids_storage = redis.Redis()
def process_payment(message: dict):
message_id = message.get('id')
# 1. Проверяем, не было ли сообщение обработано ранее
if processed_ids_storage.exists(message_id):
print(f"Сообщение {message_id} уже обработано. Пропускаем.")
return
# 2. Основная бизнес-логика
try:
# Начало транзакции в БД
with db.atomic_transaction():
# Выполняем операцию (например, списание средств)
execute_payment(message['amount'], message['user_id'])
# 3. Сохраняем ID в хранилище внутри транзакции (если возможно)
# или сразу после успешного коммита
processed_ids_storage.set(message_id, 'processed', ex=3600) # ex - время жизни ключа
print(f"Сообщение {message_id} успешно обработано.")
except Exception as e:
# В случае ошибки ID не будет сохранен, и сообщение
# может быть обработано повторно после исправления проблемы.
log_error(f"Ошибка обработки сообщения {message_id}: {e}")
Таким образом, даже если брокер доставит сообщение несколько раз, реальная бизнес-операция выполнится только при первой обработке.