Ответ
В микросервисах классические ACID-транзакции через границы сервисов невозможны. Вместо этого применяются паттерны BASE (Basically Available, Soft state, Eventually consistent) и следующие подходы:
1. Паттерн Saga: Последовательность локальных транзакций, где каждый следующий шаг зависит от успеха предыдущего. При неудаче выполняются компенсирующие транзакции.
// Пример реализации Saga с событиями
@Service
public class OrderSagaService {
@Transactional
public void createOrder(Order order) {
// 1. Локальная транзакция: создать заказ в статусе PENDING
orderRepository.save(order);
// 2. Отправить событие для резервирования товара
eventPublisher.publish(new ReserveItemsEvent(order.getId()));
}
// Обработчик успешного резервирования
@TransactionalEventListener
public void handleItemsReserved(ItemsReservedEvent event) {
// 3. Локальная транзакция: списать деньги
paymentService.charge(event.getOrderId());
// Отправить событие об успешном списании
}
// Компенсирующее действие при ошибке оплаты
@TransactionalEventListener
public void handlePaymentFailed(PaymentFailedEvent event) {
// Откат: освободить зарезервированные товары
inventoryService.releaseItems(event.getOrderId());
// Обновить статус заказа на FAILED
orderRepository.updateStatus(event.getOrderId(), OrderStatus.FAILED);
}
}
2. Паттерн Outbox: Гарантирует доставку событий в рамках локальной транзакции.
-- В рамках одной транзакции с бизнес-данными
BEGIN TRANSACTION;
INSERT INTO orders (id, amount) VALUES (123, 100.00);
INSERT INTO outbox (id, aggregate_id, event_type, payload)
VALUES (gen_random_uuid(), 123, 'OrderCreated', '{"amount":100}');
COMMIT;
-- Отдельный процесс (CDC, polling) читает outbox и публикует в брокер
3. Паттерн TCC (Try-Confirm-Cancel):
// Фаза Try: предварительное резервирование ресурсов
paymentService.reserve(orderId, amount); // Резервируем, но не списываем
inventoryService.reserve(orderId, items); // Резервируем товары
// Фаза Confirm: фиксация (идемпотентная)
paymentService.confirm(orderId); // Списание зарезервированных средств
inventoryService.confirm(orderId); // Подтверждение резерва
// Фаза Cancel: отмена (идемпотентная)
paymentService.cancel(orderId); // Отмена резерва
inventoryService.cancel(orderId); // Освобождение товаров
4. Технологии и инструменты:
- Брокеры сообщений: Kafka (с идемпотентным продюсером и exactly-once семантикой), RabbitMQ
- Оркестраторы: Camunda, Temporal, AWS Step Functions
- Мониторинг: Distributed tracing (Jaeger, Zipkin) для отслеживания потока транзакции
5. Критические требования:
- Идемпотентность: Все операции должны безопасно повторяться
- Компенсация: Для каждого действия должен быть определен компенсирующий action
- Мониторинг: Необходима видимость состояния распределенной транзакции
- Таймауты и ретраи: Стратегии обработки временных сбоев
Ответ 18+ 🔞
А, слушай, про микросервисы и транзакции! Ну это вообще отдельная песня, пиздец. Представь: раньше у тебя одна жирная база, и ты мог просто BEGIN TRANSACTION, нахуячить кучу INSERT-ов и потом COMMIT. Или ROLLBACK, если всё пошло по пизде. Красота, ACID, всё как у людей.
А теперь у тебя эти распиздяйские микросервисы, каждый со своей базой, как ёбаный феодал в своём замке. И как ты скажешь базе инвентаря и базе платежей: «Ребята, давайте синхронно, на раз-два, обновитесь, а если что — откатитесь»? Да нихуя! Это ж разные процессы, разные машины, сеть между ними, которая может в любой момент накрыться медным тазом.
Так что про классические ACID-транзакции через сервисы можно сразу забыть, как про сон. Вместо них тут царствует BASE — Basically Available, Soft state, Eventually consistent. То есть, в переводе на русский: «Вроде работает, состояние плавающее, но в конце концов, блядь, всё сойдётся». Надежда, ебать, как стратегия.
И вот какие фокусы для этого придумали, просто ёперный театр:
1. Сага (Saga) Это когда твоя транзакция — это цепочка локальных шагов. Каждый следующий пинок зависит от успеха предыдущего. А если на каком-то этапе пиздец, то надо бегать назад и откатывать всё, что наделал. Эти откаты называются «компенсирующие транзакции». Представь, ты заказал пиццу, потом пиво, потом решил посмотреть кино. А денег на кино нет. Так что придётся отменить пиво, потом пиццу, и сидеть грустный.
// Пример реализации Saga с событиями
@Service
public class OrderSagaService {
@Transactional
public void createOrder(Order order) {
// 1. Локальная транзакция: создать заказ в статусе PENDING
orderRepository.save(order);
// 2. Отправить событие для резервирования товара
eventPublisher.publish(new ReserveItemsEvent(order.getId()));
}
// Обработчик успешного резервирования
@TransactionalEventListener
public void handleItemsReserved(ItemsReservedEvent event) {
// 3. Локальная транзакция: списать деньги
paymentService.charge(event.getOrderId());
// Отправить событие об успешном списании
}
// Компенсирующее действие при ошибке оплаты
@TransactionalEventListener
public void handlePaymentFailed(PaymentFailedEvent event) {
// Откат: освободить зарезервированные товары
inventoryService.releaseItems(event.getOrderId());
// Обновить статус заказа на FAILED
orderRepository.updateStatus(event.getOrderId(), OrderStatus.FAILED);
}
}
Видишь? Создали заказ, отправили событие «зарезервируй шмотки». Если резервирование прошло — пытаемся списать бабки. Если при списании денег — пизда, то держись, собачка Муму: запускаем компенсацию и освобождаем товары. Трагедия, блядь, но что поделать.
2. Аутбокс (Outbox) Этот паттерн — просто гениальная, блядь, заглушка для совести. Проблема-то в чём? Ты в транзакции сохранил заказ в свою базу и отправил событие в Кафку. А потом твоя транзакция коммитится, а Кафка лежит. Или наоборот: событие улетело, а транзакция откатилась. Полная неконсистентность, пиздец.
Аутбокс решает это просто: ты пишешь событие в ту же самую транзакцию и в ту же самую базу, в специальную табличку. А потом уже отдельный, ебушки-воробушки, процесс (типа CDC или просто поллинг) выгребает из этой таблицы и тащит в брокер. Всё, проблема поехавших событий решена.
-- В рамках одной транзакции с бизнес-данными
BEGIN TRANSACTION;
INSERT INTO orders (id, amount) VALUES (123, 100.00);
INSERT INTO outbox (id, aggregate_id, event_type, payload)
VALUES (gen_random_uuid(), 123, 'OrderCreated', '{"amount":100}');
COMMIT;
-- Отдельный процесс (CDC, polling) читает outbox и публикует в брокер
Красота! Или закоммитилось всё, или ничего. Никаких полумер.
3. TCC (Try-Confirm-Cancel) Это как трёхфазный commit, но для бедных и умных. Вместо одной операции — три:
- Try: Предварительно зарезервируй ресурсы. Деньги на карте заморозь, товары на складе отложи в сторону. Но ещё ничего окончательно.
- Confirm: Если все этапы Try прошли успешно — финализируй. Спиши замороженные деньги, отдай отложенные товары. Эта операция должна быть идемпотентной, потому что её могут вызвать сто раз.
- Cancel: Если на любом этапе Try что-то пошло не так — отмени все резервы. Разморозь деньги, верни товары на полку. Тоже идемпотентно, ясен хуй.
// Фаза Try: предварительное резервирование ресурсов
paymentService.reserve(orderId, amount); // Резервируем, но не списываем
inventoryService.reserve(orderId, items); // Резервируем товары
// Фаза Confirm: фиксация (идемпотентная)
paymentService.confirm(orderId); // Списание зарезервированных средств
inventoryService.confirm(orderId); // Подтверждение резерва
// Фаза Cancel: отмена (идемпотентная)
paymentService.cancel(orderId); // Отмена резерва
inventoryService.cancel(orderId); // Освобождение товаров
4. Чем всё это ебашить?
- Брокеры: Кафка (с её идемпотентным продюсером и exactly-once — о, мечта!), RabbitMQ.
- Оркестраторы: Camunda, Temporal, AWS Step Functions — чтобы эти саги не расползались как тараканы.
- Мониторинг: Distributed tracing (Jaeger, Zipkin) — чтобы когда всё ебнулось, можно было проследить, в каком именно сервисе и почему начался пиздец.
5. Без чего нихуя не получится?
- Идемпотентность: Это святое. Любой вызов, любое событие должно быть безопасно обработано дважды, трижды, сто раз. Иначе ретраи тебя просто убьют.
- Компенсация: На каждое действие должен быть чёткий, прописанный откат. Не «авось пронесёт», а конкретный сценарий: «если списание денег не прошло, то освободи товары».
- Мониторинг: Ты должен видеть, в каком состоянии твоя распределённая транзакция-зомби. Иначе будешь как Герасим, который только «Муму» мычать может.
- Таймауты и ретраи: Сеть — она такая, сука. Сегодня есть, завтра нет. Надо уметь ждать и повторять, но не до бесконечности, а то задушишь систему.
Вот так, блядь, и живём. Сложно? Овердохуища. Но другого выхода нет, если хочешь масштабироваться, а не сидеть на одной монолитной базе, которая рано или поздно ляжет под нагрузкой, как загнанная лошадь.