Все проекты
Событийная архитектура / Учебный проект

EventForge

Обработка заказов через outbox и идемпотентные consumers.

В работеGo / PostgreSQL / Kafka / Docker

Интерактивная архитектура

eventforge / разбор архитектурыДемонстрация архитектуры

Доставка заказа и восстановление.

Заказ + Outbox: одна транзакция PostgreSQL

Заказ + Outbox: одна транзакция PostgreSQL

активен / записанповторсбой / отмена
Шаг 1 / 10Готов

Поступает запрос

Клиент отправляет заказ. Проследите order ID и event ID через транзакцию и доставку.

Заказ
не создан
Outbox
не записан
Бизнес-эффект
0 изменений
Kafka offset
не фиксирован
Узел / API заказов

Принимает запрос и назначает идентификаторы заказу и его событию.

Прочитать все шаги

Зафиксировать заказ, доставить событие, применить изменение и затем зафиксировать offset.

  1. Клиент отправляет заказ. Проследите order ID и event ID через транзакцию и доставку.

  2. API записывает заказ и событие outbox в одной открытой транзакции. Пока ни одна запись не зафиксирована.

  3. Commit надёжно сохраняет обе строки. При rollback relay не увидит ни одну.

  4. Relay читает зафиксированное событие. В реальном relay нужна также безопасная стратегия конкурентного захвата.

  5. Relay публикует event-42 с ключом order-42. В этой модели ключ выбирает партицию p1.

  6. После ответа брокера relay помечает запись отправленной. Эта отметка не атомарна с публикацией.

  7. Worker A владеет p1. Другие участники могут владеть p0 или p2, но не обрабатывают p1 параллельно в этой группе.

  8. Обработчик вставляет ID обработанного события. Новый ID разрешает бизнес-изменение в той же транзакции.

  9. ID и статус заказа фиксируются вместе. Бизнес-эффект один; Kafka offset ещё не фиксирован.

  10. Только после успеха БД consumer фиксирует offset. Это at-least-once с идемпотентным эффектом, не распределённая exactly-once транзакция.

EventForge / в разработке

Повторить доставку. Сохранить один эффект.

Проверьте проектируемое восстановление в интерактивной модели. Выберите сбой, затем запустите сценарий или пройдите его по шагам.

Одно событие, две доставки

Детерминированная демонстрация
Логические события
0
Доставки
0
Бизнес-изменения
0
event-42 → event-42Один IDОдно бизнес-изменение

Обзор

Проектирую учебную систему обработки заказов с transactional outbox, Kafka и идемпотентными обработчиками.

Задача

Запись заказа в базу и публикация события — независимые операции. Сбой между ними может оставить downstream-сервисы без информации о сохранённом заказе.

Мой вклад

Прорабатываю контракт доставки, окна отказа и стратегию восстановления. Backend в разработке; в этом разборе собраны архитектурные решения.

Решения и компромиссы

  • Записывать заказ и outbox в одной транзакции PostgreSQL. Relay публикует зафиксированные записи в Kafka.
  • Использовать ID заказа как ключ партиции для сохранения порядка внутри неё. Consumer groups распределяют партиции между обработчиками.
  • Атомарно сохранять ID обработанного события и бизнес-изменение. Повторять временные ошибки, а исчерпавшие попытки события отправлять в DLQ.
Компромисс

Доставка at-least-once упрощает восстановление, но допускает дубликаты. Идемпотентность требует надёжного хранилища, outbox — очистки и сопровождения relay. Порядок Kafka действует внутри партиции, не глобально.

Две записи. Окно для сбоя.

В этой схеме PostgreSQL и Kafka не разделяют прикладную транзакцию. Между записью заказа и публикацией остаётся окно: процесс может умереть после commit БД, но до отправки брокеру. Обратный порядок лишь меняет направление несогласованности.

Проектирую надёжное хранение заказа, восстановление публикации, порядок внутри партиции и безопасную повторную доставку. Цель — at-least-once доставка с идемпотентным бизнес-эффектом.

Outbox закрывает конкретное окно.

Заказ и outbox записываются одной локальной транзакцией. Relay читает зафиксированные записи, отправляет исходный ID и ключ заказа, затем отмечает подтверждение. Если он падает после отправки, запись публикуется снова. Дубликат — осознанная цена восстановления.

Для нескольких relay-workers нужны захват строк, ограничение активной работы и очистка outbox. Сравниваю polling с блокировками и CDC: более простое развёртывание против меньшего числа опросов и дополнительной инфраструктуры.

Иллюстративный SQL
-- Illustrative SQL, not extracted production code
BEGIN;
INSERT INTO orders (id, status) VALUES ($1, 'created');
INSERT INTO outbox (event_id, order_id, payload)
VALUES ($2, $1, $3);
COMMIT;
-- Publish only committed rows. Mark sent after broker ack.

Одна фиксация БД защищает два связанных факта. Публикация в Kafka происходит позже.

Сохранённое изменение может прийти снова.

Самый показательный сбой — после бизнес-транзакции, но до фиксации offset Kafka. Новый worker получает запись повторно. Уникальное ограничение на (consumer, event_id) защищает от второго эффекта; in-memory set теряет защиту при рестарте.

Метка обработанного события и бизнес-изменение должны находиться в одной транзакции. При конфликте уникальности изменение не выполняется. Offset фиксируется только после надёжного решения. Offset не является атомарным продолжением commit PostgreSQL.

Иллюстративный SQL
-- Illustrative handler transaction
BEGIN;
-- Requires UNIQUE (consumer, event_id).
WITH accepted AS (
  INSERT INTO processed_events (consumer, event_id)
  VALUES ($1, $2) ON CONFLICT DO NOTHING
  RETURNING event_id
)
UPDATE orders SET status = 'processed'
WHERE id = $3 AND EXISTS (SELECT 1 FROM accepted);
COMMIT;
-- Then commit Kafka offset; never before DB success.

Результат INSERT разрешает изменение. Если ожидаемый заказ не найден, транзакцию нужно откатить, не сохраняя метку неприменённого эффекта.

Порядок в партиции и стратегия повторов.

Стабильный ключ заказа направляет связанные события в одну партицию Kafka. В каждый момент ей владеет один участник consumer group. Это не глобальный порядок; число consumer сверх числа партиций не добавляет параллелизма топику.

Inline-повторы приостанавливают партицию и сохраняют порядок, но медленная запись блокирует следующие. Retry-топики уменьшают блокировку, меняя семантику порядка. Для DLQ необходимо подтверждение до продвижения исходного offset; поздний replay может нарушить порядок относительно последующих событий.

Проверять окна отказа.

Предлагаемая матрица backend-тестов: остановить relay после ответа брокера до отметки sent; остановить consumer после БД до offset; доставить дубликаты конкурентно; исчерпать повторы. Проверять надёжные строки и offsets, не только HTTP-ответы.

В backend-тестах планирую проверять сохранённое бизнес-состояние после каждого искусственного сбоя. Конкурентные дубликаты должны оставлять одну метку обработки и одно изменение; рестарт не должен менять этот результат.

Метрики очереди и восстановления.

Планирую отслеживать возраст старейшей outbox-записи, ошибки публикации, consumer lag, конфликты идемпотентности, повторы и возраст DLQ. Order ID и event ID связывают трассы, но не попадают в высококардинальные labels метрик.

Процедура восстановления должна определять владельца разбора DLQ, сохранение ID при replay и допустимые повторные бизнес-состояния. Без этого DLQ — хранилище, а не механизм восстановления.

Результаты и следующие шаги

В проектируемой схеме повторная доставка сохраняет ID события, а метка обработки фиксируется вместе с бизнес-изменением. Следующий этап — интеграционные backend-тесты на окна отказа.

Главный вывод

Гарантия доставки — контракт с операционными последствиями. Обработка дубликатов, повторы, наблюдаемость и восстановление должны входить в проектирование с самого начала.

Продолжить чтениеFuncWarm