Урок по треку Spring и Spring Boot 8 мин

Kafka в Spring

Топики и партиции, группы потребителей, гарантии доставки, Spring Kafka и обработка ошибок.

Kafka стоит почти в каждой вакансии Java-бэкенда. Спрашивают устройство (топики, партиции, группы), гарантии доставки и то, как Spring Kafka обрабатывает ошибки.

Модель

  • Топик разбит на партиции. Партиция это упорядоченный неизменяемый лог; у каждого сообщения свой offset.
  • Порядок гарантирован только внутри партиции. Сообщения с одинаковым ключом попадают в одну партицию (хеш ключа), поэтому события одного заказа идут по порядку, если ключ это id заказа.
  • Consumer group: каждая партиция читается ровно одним потребителем группы. Потребителей больше, чем партиций, бесполезно: лишние простаивают. Разные группы читают топик независимо.
  • Ребалансировка: при входе или выходе потребителя партиции перераспределяются.
  • Сообщения не удаляются после чтения: хранятся по времени или размеру (retention), потребитель сам помнит свой offset (коммитит в служебный топик __consumer_offsets).
  • Репликация: у партиции есть лидер и реплики, min.insync.replicas задаёт, сколько реплик должно подтвердить запись.

Продюсер

  • acks=all (по умолчанию с Kafka 3.0) ждёт подтверждения от всех синхронных реплик.
  • enable.idempotence=true (тоже по умолчанию с 3.0) убирает дубли при повторах отправки в пределах сессии продюсера.
  • Транзакции Kafka (transactional.id) дают атомарную запись в несколько партиций и exactly-once в цепочке «прочитал из Kafka, записал в Kafka».

Потребитель и гарантии

  • At-most-once: закоммитили offset до обработки, упали: сообщение потеряно.
  • At-least-once: обработали, потом закоммитили offset; упали между этим: сообщение придёт снова. Стандарт на практике.
  • Exactly-once в полном смысле достижим только внутри Kafka. С внешней БД нужен идемпотентный обработчик: хранить id обработанных сообщений или делать операции идемпотентными (upsert).

Spring Kafka

@KafkaListener(topics = "orders", groupId = "billing", concurrency = "3")
public void onOrder(OrderCreated event) {
    billing.charge(event);
}

kafkaTemplate.send("orders", order.id().toString(), new OrderCreated(...));
  • KafkaTemplate.send асинхронный и возвращает CompletableFuture<SendResult> (Spring Kafka 3). Если результат игнорировать, ошибка отправки останется незамеченной.
  • По умолчанию контейнер слушателя коммитит offset после обработки пачки (AckMode.BATCH), при MANUAL подтверждение вызывает код через Acknowledgment.
  • concurrency больше числа партиций не даёт эффекта.

Ошибки

  • DefaultErrorHandler (Spring Kafka 2.8+) повторяет сообщение с паузами (FixedBackOff, ExponentialBackOff), а после исчерпания попыток отдаёт его recoverer, обычно DeadLetterPublishingRecoverer в топик orders.DLT.
  • Неретраибельные исключения (ошибка десериализации, валидации) отправляют в DLT сразу.
  • Ошибку десериализации ловит ErrorHandlingDeserializer, иначе «ядовитое» сообщение бесконечно роняет потребителя.
  • Блокирующие повторы держат партицию: пока одно сообщение ретраится, следующие в этой партиции ждут. Неблокирующие повторы @RetryableTopic перекладывают сообщение в отдельные retry-топики, но ломают порядок.
  • Долгая обработка дольше max.poll.interval.ms (5 минут по умолчанию) выкидывает потребителя из группы, происходит ребалансировка и повторная доставка.

Частые ошибки

  • Ключ не задан или случайный, а потом удивление, что события одного заказа обрабатываются не по порядку.
  • Отправка в Kafka внутри транзакции БД до коммита (нужен outbox).
  • Неидемпотентный потребитель при at-least-once.
  • Бесконечные повторы «ядовитого» сообщения.

Что спросят на собесе

  • Что такое партиция и consumer group? Как гарантируется порядок?
  • Какие гарантии доставки бывают и как получить at-least-once?
  • Как сделать обработку идемпотентной?
  • Как Spring Kafka обрабатывает ошибки, что такое DLT?
  • Зачем ключ сообщения? Что будет, если потребителей больше, чем партиций?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.