> Использовали ли транзакции при отправке сообщений в брокер (Go)

Уровень: senior · Роль: backend · Категория: Технические вопросы

Компании: BetBoom

Стек: Go

> Пример ответа

Короткий ответ

Да, транзакции использовал, но не в классическом смысле ACID для самого брокера. В распределённых системах с message broker транзакции обычно реализуются через паттерн transactional outbox: запись события в БД и отправка в брокер выполняются атомарно через локальную транзакцию БД, а отдельный процесс (relay/publisher) доставляет сообщения. Это позволяет избежать потери сообщений при сбое между записью в БД и публикацией.

Подробное объяснение

Классические транзакции в брокере (например, Kafka transactions или JMS transactions) - это отдельный механизм, но на практике в Go-сервисах чаще используется подход с гарантией доставки через БД. Основная проблема: если сначала писать в БД, а потом отправлять в брокер, возможен сценарий, где сообщение не уйдёт, хотя бизнес-операция зафиксирована. Или наоборот - сообщение уйдёт, а транзакция БД откатится.

Transactional outbox решает это так:

  • в той же транзакции, что и бизнес-данные, пишем событие в таблицу outbox
  • после коммита отдельный воркер читает outbox и отправляет в брокер
  • после подтверждения от брокера помечает событие как отправленное

Это даёт exactly-once семантику на уровне источника. Сам брокер при этом не участвует в распределённой транзакции - мы не используем XA или двухфазный коммит, потому что это дорого и хрупко.

На практике

В реальных проектах я сталкивался с двумя подходами:

  1. Transactional outbox с polling - воркер опрашивает outbox таблицу с интервалом, отправляет батчами. Просто, но есть задержка доставки.

  2. Transactional outbox с CDC (change data capture) - например, Debezium читает WAL PostgreSQL и публикует события в Kafka. Минимальная задержка, но сложнее инфраструктура.

Также важно учитывать идемпотентность на стороне потребителя - даже с outbox возможны дубликаты при ретраях, поэтому каждое сообщение должно содержать уникальный идентификатор события.

Для самого брокера транзакции (например, Kafka producer transactions) я использовал редко - только когда нужна атомарность между несколькими топиками. Обычно это избыточно.

Пример кода

GO
// Сервис, который пишет бизнес-данные и событие в outbox атомарно
func (s *Service) CreateOrder(ctx context.Context, order Order) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// бизнес-операция
if err := s.repo.InsertOrder(ctx, tx, order); err != nil {
return err
}
// событие для брокера
event := OutboxEvent{
ID: uuid.New().String(),
AggregateID: order.ID,
Type: "order.created",
Payload: order,
CreatedAt: time.Now(),
}
if err := s.repo.InsertOutbox(ctx, tx, event); err != nil {
return err
}
return tx.Commit()
}
// Воркер, который отправляет события из outbox в брокер
func (s *Service) PublishOutbox(ctx context.Context) error {
events, err := s.repo.GetPendingOutbox(ctx, 100)
if err != nil {
return err
}
for _, event := range events {
msg, err := json.Marshal(event)
if err != nil {
continue
}
// отправка с ключом для партиционирования
err = s.producer.SendMessage(ctx, &ProducerMessage{
Topic: "orders",
Key: event.AggregateID,
Value: msg,
Headers: map[string]string{"event_id": event.ID},
})
if err != nil {
// ретрай с backoff, событие остаётся в outbox
continue
}
// помечаем как отправленное
if err := s.repo.MarkOutboxSent(ctx, event.ID); err != nil {
// возможен дубликат при ретрае - потребитель должен быть идемпотентным
continue
}
}
return nil
}

Как отвечать на собеседовании

Начни с того, что разделяешь два понятия: транзакции внутри брокера и гарантии доставки между БД и брокером. Скажи, что в большинстве Go-сервисов используется transactional outbox, потому что он проще и надёжнее распределённых транзакций. Упомяни, что сам брокер может поддерживать транзакции (Kafka, RabbitMQ), но это редко нужно на практике. Подчеркни важность идемпотентности потребителя и обработки дубликатов. Если спросят про альтернативы - упомяни CDC и двухфазный коммит, объясни trade-off каждого.

Что проверяет интервьюер

Интервьюер хочет понять, насколько ты осознаёшь проблемы распределённых систем: атомарность между хранилищем и брокером, гарантии доставки, обработка сбоев. Также проверяется знание паттернов (outbox, CDC, saga) и умение обосновать выбор. Важно показать, что ты не используешь транзакции брокера "по привычке", а понимаешь, когда они нужны, а когда нет.

Типичные ошибки

  • Утверждение, что можно просто обернуть запись в БД и отправку в брокер в одну транзакцию - это невозможно без распределённой транзакции.
  • Игнорирование дубликатов при ретраях - нужно всегда проектировать потребителя идемпотентным.
  • Использование транзакций брокера без необходимости - это добавляет сложность и снижает производительность.
  • Отсутствие обработки ошибок при отправке из outbox - воркер должен уметь ретраить и не терять события.
  • Непонимание разницы между at-least-once и exactly-once семантикой.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью