Транзакционный месседжинг
Транзакционный месседжинг — это набор архитектурных приемов, которые обеспечивают согласованность между локальными изменениями состояния (БД/кэш) и сообщениями в брокере/шине. Цель: «состояние зафиксировано ↔ сообщение не потеряно и не продублировано» при сбоях, ретраях, масштабировании и мульти-тенантности.
1) Семантика доставки
At-most-once: быстро и дешево, возможны потери, дублей нет.
At-least-once: не теряет сообщения, возможны дубли → требуется идемпотентность.
(Эффективное) Exactly-once: нет потерь и нет видимых дублей для бизнес-эффектов, достигается комбинацией техник (outbox/inbox, транзакции продьюсера/консьюмера, дедуп).
2) Почему «двухписьмо» опасно
Наивная логика «сначала запишем в БД, потом отправим в шину» (или наоборот) рвется при падении между шагами: данные зафиксированы, а событие потеряно; либо событие ушло, а данных нет. Транзакционный месседжинг устраняет этот разрыв.
3) Базовые паттерны
3.1 Outbox (производитель)
В одной локальной транзакции записываем бизнес-изменение и строку в таблицу `outbox`; отдельный паблишер читает outbox и публикует в брокер с ретраями и backoff. Потери исключены; дубли гасим идемпотентностью у потребителей.
3.2 Inbox/Idempotent Consumer (потребитель)
Перед выполнением эффекта консьюмер делает `INSERT` в `inbox(consumer, event_id)` как первичный ключ. Конфликт ключа = событие уже обработано → пропускаем. Так достигается «эффективное exactly-once».
3.3 Read-Process-Write с транзакцией оффсета
Шаблон для лог-ориентированных шин: консьюмер читает батч, в той же транзакции фиксирует бизнес-изменение и «пройденный оффсет». После коммита брокер считает сообщения потребленными. Это устраняет «прочитали → упали → повторили» без дублей в эффекте.
3.4 TCC/Саги для межсервисных эффектов
Когда нужен согласованный мультишаговый процесс, используем TCC или саги; сообщения — транспорт команд/событий, а транзакционность — на уровне шагов и компенсаций.
4) Идемпотентные продьюсеры и консьюмеры
Продьюсер: стабильный `message_id`/`idempotency_key`, повторная отправка с тем же ключом не создает новых эффектов у подписчиков; поддерживайте последовательность (sequence) по ключу.
Консьюмер: `inbox` + бизнес-идемпотентность (upsert/merge, проверка последней версии/ревизии).
5) Порядок и причинность
Партиционируйте по бизнес-ключу (например, `aggregate_id`, `tenant_id`), чтобы события одного объекта приходили в порядке.
Внутри партиции сохраняйте последовательные номера/временные метки; при редрайве из DLQ соблюдайте «по ключу и последовательно».
Если глобальный порядок не критичен, обеспечивайте локальный порядок по ключу и фиксируйте инварианты домена.
6) Оффсеты и фиксация эффектов
Вариант A: «Оффсет в БД»
Записывайте «последний обработанный оффсет (partition, offset)» в ту же транзакцию, где меняете доменные данные. При рестарте продолжите с следующего оффсета, избегая повторного эффекта.
Вариант B: «Транзакция брокера»
Некоторые брокеры поддерживают атомарную запись сообщений и оффсетов в рамках одной транзакции продьюсера/консьюмера. Используйте, если доступно, но всегда дополняйте идемпотентностью на потребителе.
7) Ретраи, backoff, DLQ
Повторяйте только ретраибл-ошибки (таймауты, 5xx), с экспоненциальным backoff и джиттером.
Нон-ретраибл (schema/валидация) — сразу в DLQ с метаданными (tenant, key, offset, причина).
Редрайв из DLQ дозируйте (batch, rate limit), проверяйте схему перед повтором, соблюдайте порядок по ключу.
8) Мульти-тенантность и регионы
Включайте `tenant_id`, `plan`, `region` в метаданные сообщения и ключи партиционирования.
Per-tenant fairness: лимитируйте публикацию/обработку, чтобы «шумный» клиент не выел бюджет у остальных.
Residency: храните сообщения и outbox в том же регионе, что и доменные данные; межрегиональные репликации — асинхронные агрегаты.
9) Наблюдаемость и аудит
Трейсинг: корреляция `event_id`/`aggregate_id`/`saga_id`, спаны «read → process → write/commit».
Метрики: лаг публикации/обработки (p95/p99), доля успехов, DLQ-rate, успех редрайва, «дубликаты подавлены».
Логи: коротко на успех; подробно на ошибки (причина, попытка, ключ, оффсет).
Аудит: кто редрайвил/откатывал, каким батчем и с каким результатом.
10) Безопасность и соответствие
Минимизируйте PII в payload; маскируйте при переносе в DLQ/логи.
Подписывайте/шифруйте сообщения для внешних шин; используйте mTLS между сервисами.
Управляйте сроком хранения и «правом на забвение» per tenant/region.
11) Типовые схемы интеграции
1. Сервис-источник (write-side)
Локальная транзакция: доменная запись + outbox.
Паблишер: батчи, `SKIP LOCKED`, backoff, лимиты per tenant.
Мониторинг лага `now − occurred_at`.
2. Сервис-потребитель (read-side)
Чтение батча → попытка `INSERT inbox(consumer, event_id)` → при успехе выполняем эффект.
В той же транзакции фиксируем «пройденный оффсет» (вариант A) или полагаемся на транзакцию брокера (вариант B).
На ошибке: ретрай или DLQ по политике.
3. Проекция/материализованный вид
Только идемпотентные апдейты (upsert), компактные ключи дедупа, периодическая сверка контрольных сумм.
12) Конфигурационные шаблоны (пример)
yaml producer:
idempotency_key: event_id partition_key: "{tenant_id}:{aggregate_id}"
retry:
max_attempts: 8 initial_ms: 200 max_ms: 8000 strategy: exponential_full_jitter
consumer:
batch: 500 offset_commit: "with_domain_tx" # или "broker_tx"
inbox_enabled: true concurrency_per_partition: 4 dlq:
enabled: true batch_redrive: 200 rate_limit_per_sec: 50 order_by_key: true
observability:
metrics:
- processing_lag_ms
- publish_success_ratio
- dlq_rate
- redrive_success_ratio tracing_tags: [event_id, tenant_id, aggregate_id, partition, offset]
13) Чек-лист перед продом
- Устранено «двухписьмо»: outbox на продьюсере или фиксация оффсета и эффекта в одной транзакции у консьюмера.
- Идемпотентный консьюмер: `inbox`/дедуп-журнал, бизнес-идемпотентность операций.
- Партиционирование по бизнес-ключу, локальный порядок соблюден.
- Ретраи с backoff+джиттером, классификация ошибок, DLQ с богатыми метаданными.
- Редрайв дозированный, безопасный; есть плейбуки.
- Мульти-тенантные лимиты и приоритеты; теги `tenant_id/plan/region`.
- Телеметрия: лаги, доля успехов, «дубликаты подавлены», алерты по p95/p99.
- Политики PII/ретеншна/шифрования соблюдены.
- Тесты: падение между шагами, дубликаты, порядок по ключу, массовый редрайв.
14) Типичные ошибки
Отправка в шину и запись в БД раздельными шагами без outbox/транзакции оффсета.
Консьюмер без идемпотентности → дублирует побочные эффекты.
Глобальный порядок «во что бы то ни стало» — дорог и редко оправдан; достаточно порядка по ключу.
Массовый редрайв без лимитов → вторичный инцидент.
Отсутствие трейсинга/лаг-метрик → «скрытая деградация».
Смешение PII в DLQ/логах.
15) Быстрые рецепты
SaaS-события: Outbox + идемпотентный консьюмер (inbox), партиционирование по `tenant_id:aggregate_id`.
ETL/проекции: Read-process-write с фиксацией оффсетов в одной транзакции, батчи 500–1000, upsert.
Высокая нагрузка: шардирование паблишеров, `SKIP LOCKED`, WFQ per tenant, контроль лага.
Строгая комплаенс-зона: региональные outbox, шифрование payload, ретеншн и аудит редрайвов.
Заключение
Транзакционный месседжинг — это дисциплина соединения данных и сообщений. Комбинируя outbox/inbox, идемпотентность, фиксацию оффсетов вместе с эффектами и управляемые ретраи с DLQ, вы получаете практическое exactly-once-поведение без глобальных блокировок и сохраняете SLO даже при сбоях, пиках и сложной мульти-тенантной эксплуатации.