> Как отправляются события об изменении статусов? (Go)
Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы
Компании: InDrive
Стек: Go
> Пример ответа
Короткий ответ
События об изменении статусов отправляются через механизм событийного взаимодействия: сервис публикует событие в брокер сообщений (Kafka, NATS, RabbitMQ) или через внутреннюю шину, а подписчики обрабатывают его асинхронно. В Go это реализуется через паттерн outbox + publisher, чтобы гарантировать атомарность записи в БД и публикации. Альтернатива - синхронные webhook-вызовы или gRPC-stream, но они менее надёжны и масштабируемы.
Подробное объяснение
Основная проблема при отправке событий - согласованность между изменением статуса в базе данных и публикацией события. Если сначала обновить БД, а потом отправить событие, возможна потеря при сбое. Если наоборот - подписчики увидят событие до фактического изменения.
Стандартное решение - transactional outbox: в той же транзакции, где меняется статус, пишем запись в таблицу outbox. Отдельный процесс (relay) читает outbox и публикует события в брокер. Это даёт at-least-once доставку, поэтому подписчики должны быть идемпотентными.
Второй вариант - CDC (Change Data Capture) через Debezium: слушаем изменения в WAL PostgreSQL и транслируем их в Kafka. Это удобно, но добавляет инфраструктурную сложность.
Для внутренних событий в рамках одного процесса можно использовать простой pub/sub на каналах Go, но это не масштабируется на несколько инстансов.
Формат события: обычно JSON или protobuf, с полями event_id, entity_id, old_status, new_status, timestamp, version. Версионирование обязательно - подписчики могут отставать.
На практике
В Go-сервисах чаще всего используют:
- Kafka через
segmentio/kafka-goилиconfluent-kafka-go - NATS JetStream для лёгких сценариев
- outbox-реализации:
watermill(с поддержкой outbox),go-outbox, или собственная реализация наpgx+pgxpool
Ретри-логика: при ошибке публикации relay должен делать backoff и не терять события. Подписчики обрабатывают события идемпотентно - по event_id или по ключу entity_id + new_status.
Важно: не отправлять события в обход outbox, даже если кажется, что "сейчас просто". Это приводит к рассинхронизации.
Пример кода
GO// outbox recordtype OutboxEvent struct {ID int64EventID stringEntityID stringStatus stringPayload []byteCreatedAt time.Time}// publish in transactionfunc ChangeStatus(ctx context.Context, db *pgxpool.Pool, entityID string, newStatus string) error {tx, err := db.Begin(ctx)if err != nil {return err}defer tx.Rollback(ctx)// update status_, err = tx.Exec(ctx, `UPDATE entities SET status = $1 WHERE id = $2`, newStatus, entityID)if err != nil {return err}// write outbox_, err = tx.Exec(ctx, `INSERT INTO outbox (event_id, entity_id, status, payload, created_at)VALUES ($1, $2, $3, $4, now())`,uuid.New().String(), entityID, newStatus, []byte(`{"source":"service-a"}`))if err != nil {return err}return tx.Commit(ctx)}// relay loopfunc Relay(ctx context.Context, db *pgxpool.Pool, producer *kafka.Writer) {for {rows, err := db.Query(ctx, `SELECT id, event_id, entity_id, status, payloadFROM outboxWHERE published_at IS NULLORDER BY idLIMIT 100`)if err != nil {log.Printf("query outbox: %v", err)time.Sleep(time.Second)continue}var events []OutboxEventfor rows.Next() {var e OutboxEventrows.Scan(&e.ID, &e.EventID, &e.EntityID, &e.Status, &e.Payload)events = append(events, e)}rows.Close()for _, e := range events {msg := kafka.Message{Key: []byte(e.EntityID),Value: e.Payload,}if err := producer.WriteMessages(ctx, msg); err != nil {log.Printf("publish failed: %v", err)break}db.Exec(ctx, `UPDATE outbox SET published_at = now() WHERE id = $1`, e.ID)}time.Sleep(500 * time.Millisecond)}}
Как отвечать на собеседовании
Начни с главного - почему нельзя просто отправить событие после изменения БД. Покажи понимание проблемы согласованности. Затем опиши outbox как базовое решение, упомяни альтернативы (CDC, синхронные вызовы) и их trade-off.
Подчеркни практические детали: идемпотентность подписчиков, ретраи, версионирование событий, порядок доставки (если важен). Если спросят про конкретный брокер - сравни Kafka и NATS по задержке и гарантиям.
Не углубляйся в детали реализации конкретной библиотеки, если не просят. Лучше показать архитектурное мышление.
Что проверяет интервьюер
- Понимание распределённых систем и проблемы согласованности
- Знание паттернов интеграции (outbox, CDC, event sourcing)
- Практический опыт с брокерами сообщений
- Умение проектировать надёжные асинхронные потоки
- Понимание идемпотентности и обработки ошибок
Типичные ошибки
- Предложение отправлять событие сразу после
UPDATEбез outbox - сразу показывает непонимание гарантий - Игнорирование идемпотентности подписчиков
- Утверждение, что Kafka гарантирует exactly-once - это не так в общем случае
- Смешивание синхронного и асинхронного взаимодействия без чёткого обоснования
- Отсутствие упоминания версионирования событий
- Предложение использовать
sync.Mutexили каналы для межсервисных событий - это работает только в одном процессе
> Похожие задачи по Go
Какие гарантии доставки сообщений предоставляет Google Pub/Sub?
Какой механизм используется для отправки событий?
Как хранится история переходов статусов платежей?
Вы используете один кластер или геораспределенную систему?
> Похожие задачи по backend
Какие гарантии доставки сообщений предоставляет Google Pub/Sub?
Какой механизм используется для отправки событий?
Как хранится история переходов статусов платежей?
Вы используете один кластер или геораспределенную систему?
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью