Как обеспечивалась транзакционность при работе с сообщениями (например, в Kafka) в вашем проекте?

Ответ

В проекте использовался паттерн «транзакционность базы данных и отправки сообщений» для гарантии согласованности между состоянием БД и отправленными событиями.

Основной подход (Spring Kafka + @Transactional):

@Service
public class OrderService {
    @Transactional // Транзакция управляется Spring
    public void createOrder(Order order) {
        // 1. Сохраняем заказ в БД
        orderRepository.save(order);
        // 2. Отправляем событие в Kafka.
        // Если на предыдущем шаге произойдет исключение, транзакция откатится и сообщение не будет отправлено.
        kafkaTemplate.send("order-events", order.getId(), new OrderCreatedEvent(order));
    }
}

Для работы требуется настройка KafkaTransactionManager и ChainedKafkaTransactionManager (или JpaTransactionManager) для объединения транзакций.

Паттерн Transactional Outbox для повышенной надежности:

  1. Событие сохраняется в ту же БД-транзакцию в специальную таблицу outbox.
  2. Отдельный процесс (например, Debezium или scheduled task) читает из outbox и публикует события в Kafka. Это гарантирует доставку даже при длительных простоях брокера.

Важно: Этот подход обеспечивает «at-least-once» доставку. Для идемпотентности потребителей необходимо использовать уникальные идентификаторы сообщений (idempotent producer).

Ответ 18+ 🔞

Слушай, вот сидишь ты, пишешь свой микросервис, и тут тебе приспичило, чтобы заказ в базе сохранился и событие в Кафку улетело, да так, чтобы либо всё, либо нихуя. Классика жанра, блядь.

Ну, думаешь, чего проще — накинул @Transactional на метод, и порядок. Сохранил заказ, отправил событие, и если где-то посередке пиздец случился, то спринг всё откатит, как будто ничего и не было. Красота!

@Service
public class OrderService {
    @Transactional // Магия спринга, которая говорит: "Всё или ничего, сука!"
    public void createOrder(Order order) {
        // 1. Тыкаем заказ в базу. Пока что только в памяти Hibernate.
        orderRepository.save(order);
        // 2. Шлём весточку в Кафку, что заказ родился.
        // Если на шаге 1.5 случится пиздец (например, констрейнт базы), то транзакция откатится и это сообщение в Кафку НЕ улетит. В теории.
        kafkaTemplate.send("order-events", order.getId(), new OrderCreatedEvent(order));
    }
}

Но тут, ёпта, подвох! Чтобы эта магия сработала, надо эти две транзакции — в базу и в Кафку — связать в одну цепь, как каторжников. Настраиваешь какого-нибудь ChainedKafkaTransactionManager, и он за тебя следит, чтобы откат в одном месте потянул за собой откат в другом. А иначе получится, что заказ не сохранился, а событие уже улетело — и пошла пизда по кочкам, все системы думают, что заказ есть, а его на самом деле нет. Кошмар, блядь!

Но это, скажу я тебе, подход для смелых и немного наивных. Потому что если твой брокер Кафки вдруг накрылся медным тазом в самый момент отправки, то вся эта красивая конструкция может ебнуться, даже не начавшись.

Поэтому умные дядьки придумали паттерн «Transactional Outbox». Суть проста, как три рубля:

  1. В рамках той же самой, ебучей, транзакции с базой ты пишешь событие не сразу в Кафку, а в специальную табличку в этой же базе. Всё в одной транзакции — либо заказ И событие сохранятся, либо нихуя. Классическая атомарность, блядь.
  2. А потом уже отдельный, долбоёбистый воркер (типа Дебезиума или просто шедулед таска) выгребает из этой таблички события и тыкает их в Кафку. Да хоть сутки брокер лежит — как только воскреснет, воркер всё доотправляет.

Важный момент, на котором все обжигаются: этот подход гарантирует «at-least-once» доставку. Это значит, что одно и то же событие может прилететь в твой топик два, три, хуй знает сколько раз. Поэтому потребители должны быть идемпотентными — получать одно и то же событие сто раз и реагировать так, будто оно пришло один раз. Или используй idempotent producer на стороне отправителя, чтобы Кафка сама отсекала дубли. Иначе — ну ты понял, пиздец и бардак.