Ответ
Для решения этой задачи используется паттерн Transactional Outbox. Его суть — избежать несогласованного состояния, когда операция в базе данных прошла успешно, а отправка сообщения в очередь — нет (или наоборот).
Принцип работы
- Единая транзакция: В рамках одной транзакции с основной бизнес-логикой (например, создание заказа) запись о событии помещается в специальную таблицу в той же базе данных —
outbox. - Асинхронный отправитель: Отдельный независимый процесс (реле, poller) периодически опрашивает таблицу
outboxна наличие необработанных записей. - Доставка и подтверждение: Процесс-отправитель читает событие из таблицы, отправляет его в брокер сообщений (Kafka, RabbitMQ) и только после успешного подтверждения от брокера помечает запись в
outboxкак обработанную (или удаляет ее).
Пример реализации (псевдокод на Python)
# Шаг 1: Основная бизнес-логика
def create_order(order_data):
# Начало транзакции
with db.atomic_transaction():
# 1. Сохраняем заказ в основной таблице
new_order = Order.create(**order_data)
# 2. Создаем запись о событии в таблице outbox
OutboxMessage.create(
topic='orders.created',
payload={'order_id': new_order.id, 'customer_id': new_order.customer_id}
)
# Конец транзакции. Либо обе записи сохранятся, либо ни одна.
# Шаг 2: Отдельный процесс-отправитель
def process_outbox_messages():
while True:
messages = OutboxMessage.get_unsent(limit=100)
for msg in messages:
try:
# 3. Отправляем сообщение в брокер
message_broker.publish(topic=msg.topic, body=msg.payload)
# 4. Помечаем как отправленное
msg.mark_as_sent()
except Exception as e:
# Логируем ошибку, сообщение будет обработано в следующий раз
log_error(f"Failed to send message {msg.id}: {e}")
time.sleep(5) # Пауза между опросами
Преимущества
- Надежность: Гарантирует, что ни одно событие не будет потеряно при сбое одной из систем.
- Разделение ответственностей: Бизнес-логика не зависит от доступности брокера сообщений.
- Согласованность данных: Обеспечивает целостность данных между локальной БД и внешними системами.