Как сделаешь атомарной логику взаимодействия с микросервисами
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Атомарность в микросервисной архитектуре
Атомарность между микросервисами — это одна из самых сложных проблем в распределённых системах. Идеальный ACID транспорт невозможен, поэтому используются различные паттерны для достижения консистентности данных.
Проблема: классическая транзакция не работает
// В монолите это просто
@Transactional
public void createOrder(OrderRequest request) {
Order order = orderRepository.save(request.toOrder());
inventoryService.reserve(order.getItems()); // Если упадёт, откатится
paymentService.charge(order.getPayment()); // Если упадёт, откатится
}
// В микросервисах это невозможно
// OrderService и InventoryService имеют разные БД
// Нельзя выполнить транзакцию через сеть
Паттерн 1: Saga Pattern (Сага)
Saga — это последовательность шагов, где каждый шаг — это отдельная транзакция в разных сервисах.
A. Choreography (Хорегография)
Сервисы общаются через события, каждый реагирует на события других.
// Order Service
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private OrderRepository orderRepository;
@Transactional
public void createOrder(OrderRequest request) {
// Шаг 1: Создаём заказ (локальная транзакция)
Order order = new Order();
order.setItems(request.getItems());
order.setStatus("PENDING");
order = orderRepository.save(order); // Коммитится
// Шаг 2: Публикуем событие для других сервисов
OrderCreatedEvent event = new OrderCreatedEvent(order.getId(), order.getItems());
rabbitTemplate.convertAndSend("order-exchange", "order.created", event);
}
}
// Inventory Service слушает и реагирует
@Service
public class InventoryService {
@Autowired
private InventoryRepository inventoryRepository;
@Autowired
private RabbitTemplate rabbitTemplate;
@RabbitListener(queues = "inventory-queue")
@Transactional
public void handleOrderCreated(OrderCreatedEvent event) {
try {
// Шаг 1: Резервируем товар (локальная транзакция)
for (OrderItem item : event.getItems()) {
Inventory inv = inventoryRepository.findByProductId(item.getProductId())
.orElseThrow();
if (inv.getAvailable() < item.getQuantity()) {
// Не хватает товара!
throw new OutOfStockException();
}
inv.setAvailable(inv.getAvailable() - item.getQuantity());
inventoryRepository.save(inv); // Коммитится
}
// Шаг 2: Публикуем событие успеха
InventoryReservedEvent reserved = new InventoryReservedEvent(event.getOrderId());
rabbitTemplate.convertAndSend("order-exchange", "inventory.reserved", reserved);
} catch (OutOfStockException e) {
// Шаг 2: Публикуем событие отката
OrderFailedEvent failed = new OrderFailedEvent(event.getOrderId(), "Out of stock");
rabbitTemplate.convertAndSend("order-exchange", "order.failed", failed);
}
}
}
// Payment Service также слушает
@Service
public class PaymentService {
@Autowired
private PaymentRepository paymentRepository;
@Autowired
private RabbitTemplate rabbitTemplate;
@RabbitListener(queues = "payment-queue")
@Transactional
public void handleInventoryReserved(InventoryReservedEvent event) {
try {
// Шаг 1: Обрабатываем платёж (локальная транзакция)
Payment payment = new Payment();
payment.setOrderId(event.getOrderId());
payment.setStatus("PROCESSING");
paymentRepository.save(payment);
// Имитируем обработку платежа
boolean success = chargeCard(event.getOrderId());
if (success) {
payment.setStatus("SUCCESS");
paymentRepository.save(payment);
// Шаг 2: Публикуем событие успеха
PaymentSuccessEvent pse = new PaymentSuccessEvent(event.getOrderId());
rabbitTemplate.convertAndSend("order-exchange", "payment.success", pse);
} else {
throw new PaymentFailedException();
}
} catch (PaymentFailedException e) {
// Шаг 2: Публикуем событие отката
PaymentFailedEvent pfe = new PaymentFailedEvent(event.getOrderId());
rabbitTemplate.convertAndSend("order-exchange", "payment.failed", pfe);
}
}
}
// Order Service слушает результаты
@RabbitListener(queues = "order-completion-queue")
@Transactional
public void handlePaymentSuccess(PaymentSuccessEvent event) {
Order order = orderRepository.findById(event.getOrderId()).orElseThrow();
order.setStatus("CONFIRMED");
orderRepository.save(order);
}
@RabbitListener(queues = "order-failure-queue")
@Transactional
public void handlePaymentFailed(PaymentFailedEvent event) {
Order order = orderRepository.findById(event.getOrderId()).orElseThrow();
order.setStatus("FAILED");
orderRepository.save(order);
// ОТКАТ: нужно вернуть товар на склад!
OrderRollbackEvent rollback = new OrderRollbackEvent(event.getOrderId());
rabbitTemplate.convertAndSend("order-exchange", "order.rollback", rollback);
}
Проблемы Choreography:
- Сложно отследить поток операций
- Циклические зависимости между сервисами
- Трудно добавлять новые сервисы
B. Orchestration (Оркестрация)
Центральный Saga Orchestrator координирует все шаги.
// Saga Orchestrator
@Service
public class OrderSagaOrchestrator {
@Autowired
private OrderService orderService;
@Autowired
private InventoryService inventoryService;
@Autowired
private PaymentService paymentService;
@Autowired
private SagaRepository sagaRepository;
public Order executeOrderSaga(OrderRequest request) {
// Создаём запись о Саге (для отката)
Saga saga = new Saga();
saga.setOrderId(UUID.randomUUID().toString());
saga.setStatus("STARTED");
sagaRepository.save(saga);
try {
// Шаг 1: Создаём заказ
Order order = orderService.createOrder(request);
saga.addStep("ORDER_CREATED", order.getId());
sagaRepository.save(saga);
// Шаг 2: Резервируем товар
inventoryService.reserveInventory(order.getId(), request.getItems());
saga.addStep("INVENTORY_RESERVED", order.getId());
sagaRepository.save(saga);
// Шаг 3: Обрабатываем платёж
paymentService.chargePayment(order.getId(), order.getAmount());
saga.addStep("PAYMENT_PROCESSED", order.getId());
// Все успешно
saga.setStatus("COMPLETED");
sagaRepository.save(saga);
return order;
} catch (Exception e) {
// ОТКАТ в обратном порядке
rollbackSaga(saga);
throw e;
}
}
private void rollbackSaga(Saga saga) {
// Откатываем в обратном порядке
List<SagaStep> steps = saga.getSteps();
for (int i = steps.size() - 1; i >= 0; i--) {
SagaStep step = steps.get(i);
try {
if ("PAYMENT_PROCESSED".equals(step.getName())) {
paymentService.refundPayment(step.getOrderId());
} else if ("INVENTORY_RESERVED".equals(step.getName())) {
inventoryService.releaseInventory(step.getOrderId());
} else if ("ORDER_CREATED".equals(step.getName())) {
orderService.cancelOrder(step.getOrderId());
}
} catch (Exception rollbackError) {
// Логируем ошибку отката (для ручного вмешательства)
saga.addFailedRollback(step.getName(), rollbackError.getMessage());
}
}
saga.setStatus("ROLLED_BACK");
sagaRepository.save(saga);
}
}
Паттерн 2: Compensating Transactions (Компенсирующие транзакции)
Для каждой успешной операции есть компенсирующая операция на случай отката.
@Service
public class CompensatingTransactionService {
// Основная операция
@Transactional
public void chargePayment(String orderId, double amount) {
Payment payment = new Payment();
payment.setOrderId(orderId);
payment.setAmount(amount);
payment.setStatus("CHARGED");
paymentRepository.save(payment);
}
// Компенсирующая операция (откат)
@Transactional
public void refundPayment(String orderId) {
Payment payment = paymentRepository.findByOrderId(orderId).orElseThrow();
payment.setStatus("REFUNDED");
paymentRepository.save(payment);
// Возвращаем деньги клиенту
Refund refund = new Refund();
refund.setPaymentId(payment.getId());
refund.setAmount(payment.getAmount());
refundRepository.save(refund);
}
}
Паттерн 3: Event Sourcing + CQRS
Сохраняем все события в Event Store, можем воспроизвести состояние в любой момент.
// Событие
public class OrderEvent {
private String orderId;
private String eventType; // OrderCreated, PaymentProcessed, etc.
private String payload;
private Instant timestamp;
}
@Service
public class OrderEventService {
@Autowired
private EventStore eventStore;
@Transactional
public void createOrder(OrderRequest request) {
// Сохраняем событие (вместо состояния)
OrderEvent event = new OrderEvent();
event.setOrderId(UUID.randomUUID().toString());
event.setEventType("OrderCreated");
event.setPayload(json(request));
event.setTimestamp(Instant.now());
eventStore.save(event);
// Публикуем для других сервисов
eventPublisher.publish(event);
}
public Order getOrderState(String orderId) {
// Воспроизводим состояние из всех событий
List<OrderEvent> events = eventStore.findByOrderId(orderId);
Order order = new Order();
for (OrderEvent event : events) {
if ("OrderCreated".equals(event.getEventType())) {
order.setStatus("PENDING");
} else if ("PaymentProcessed".equals(event.getEventType())) {
order.setStatus("PAID");
} else if ("OrderConfirmed".equals(event.getEventType())) {
order.setStatus("CONFIRMED");
}
}
return order;
}
}
Паттерн 4: Idempotency Keys (Идемпотентность)
Если один сервис отправит запрос дважды, результат должен быть одинаковым.
@Service
public class PaymentService {
@Autowired
private PaymentRepository paymentRepository;
@Transactional
public PaymentResult processPayment(String orderId, double amount, String idempotencyKey) {
// Проверяем, не обработали ли уже с этим ключом
PaymentResult existing = paymentRepository.findByIdempotencyKey(idempotencyKey)
.orElse(null);
if (existing != null) {
// Возвращаем старый результат (идемпотентно)
return existing;
}
// Первый раз: обрабатываем платёж
PaymentResult result = new PaymentResult();
result.setOrderId(orderId);
result.setAmount(amount);
result.setIdempotencyKey(idempotencyKey);
result.setStatus("SUCCESS");
paymentRepository.save(result);
return result;
}
}
// Использование
public ResponseEntity<?> createOrder(@RequestBody OrderRequest request) {
String idempotencyKey = request.getIdempotencyKey(); // UUID от клиента
PaymentResult result = paymentService.processPayment(
order.getId(),
order.getAmount(),
idempotencyKey
);
return ResponseEntity.ok(result);
}
Таблица сравнения паттернов
| Паттерн | Сложность | Откат | Консистентность | Когда использовать |
|---|---|---|---|---|
| Saga Choreography | Средняя | Компенсирующие | Eventual | Простые процессы |
| Saga Orchestration | Высокая | Явный откат | Eventual | Сложные процессы |
| Event Sourcing | Высокая | Полная история | Strong | Критичные данные |
| Idempotency | Низкая | Автоматический | Strong | Все REST API |
Best Practice: Комбинирование паттернов
@Service
public class RobustOrderService {
// 1. Используем Saga для оркестрации
@Autowired
private OrderSagaOrchestrator sagaOrchestrator;
// 2. Используем идемпотентность для безопасности
@Transactional
public Order createOrder(OrderRequest request, String idempotencyKey) {
// Проверяем, не создали ли уже
Order existing = orderRepository.findByIdempotencyKey(idempotencyKey)
.orElse(null);
if (existing != null) {
return existing; // Идемпотентный результат
}
// 3. Выполняем Saga
Order order = sagaOrchestrator.executeOrderSaga(request);
order.setIdempotencyKey(idempotencyKey);
return orderRepository.save(order);
}
}
Итог
Полная атомарность между микросервисами невозможна. Нужно выбрать один из паттернов или комбинацию:
- Saga — для последовательных операций
- Event Sourcing — для критичных данных
- Idempotency — для безопасности API
- Compensating transactions — для отката
Цель — достичь eventual consistency: в конечном итоге система будет в согласованном состоянии, даже если временно данные разошлись.