Ответ
В проекте использовался паттерн «транзакционность базы данных и отправки сообщений» для гарантии согласованности между состоянием БД и отправленными событиями.
Основной подход (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 для повышенной надежности:
- Событие сохраняется в ту же БД-транзакцию в специальную таблицу
outbox. - Отдельный процесс (например, 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». Суть проста, как три рубля:
- В рамках той же самой, ебучей, транзакции с базой ты пишешь событие не сразу в Кафку, а в специальную табличку в этой же базе. Всё в одной транзакции — либо заказ И событие сохранятся, либо нихуя. Классическая атомарность, блядь.
- А потом уже отдельный, долбоёбистый воркер (типа Дебезиума или просто шедулед таска) выгребает из этой таблички события и тыкает их в Кафку. Да хоть сутки брокер лежит — как только воскреснет, воркер всё доотправляет.
Важный момент, на котором все обжигаются: этот подход гарантирует «at-least-once» доставку. Это значит, что одно и то же событие может прилететь в твой топик два, три, хуй знает сколько раз. Поэтому потребители должны быть идемпотентными — получать одно и то же событие сто раз и реагировать так, будто оно пришло один раз. Или используй idempotent producer на стороне отправителя, чтобы Кафка сама отсекала дубли. Иначе — ну ты понял, пиздец и бардак.