← Назад к вопросам

Как откатить операцию в микросервисной архитектуре

3.0 Senior🔥 131 комментариев
#REST API и микросервисы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Откатывание операций в микросервисной архитектуре

В микросервисной архитектуре нет встроенной ACID транзакции, которая охватывает несколько сервисов. Откатывание (rollback) операции требует специальных стратегий.

1. Сага-паттерн (Saga Pattern)

Сага — это распределённая транзакция, состоящая из последовательности локальных транзакций.

Хореография (Choreography)

Каждый сервис публикует события, другие сервисы слушают и реагируют.

// OrderService
@Service
public class OrderService {
    @Autowired
    private EventPublisher eventPublisher;
    @Autowired
    private OrderRepository orderRepository;
    
    @Transactional
    public void createOrder(OrderRequest request) {
        Order order = new Order(request);
        orderRepository.save(order);
        
        // Опубликовать событие
        eventPublisher.publish(
            new OrderCreatedEvent(order.getId(), order.getTotalAmount())
        );
    }
}

// PaymentService слушает события
@Service
public class PaymentService {
    @Autowired
    private PaymentRepository paymentRepository;
    @Autowired
    private EventPublisher eventPublisher;
    
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        try {
            Payment payment = processPayment(event.getOrderId(), event.getAmount());
            paymentRepository.save(payment);
            
            eventPublisher.publish(
                new PaymentSuccessEvent(event.getOrderId())
            );
        } catch (PaymentException e) {
            // Опубликовать событие отката
            eventPublisher.publish(
                new PaymentFailedEvent(event.getOrderId())
            );
        }
    }
}

// OrderService слушает событие отката
@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    
    @EventListener
    public void handlePaymentFailed(PaymentFailedEvent event) {
        // Откатить заказ
        Order order = orderRepository.findById(event.getOrderId()).orElseThrow();
        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.save(order);
    }
}

Преимущества:

  • Свободная связь между сервисами
  • Асинхронная обработка

Недостатки:

  • Сложно отследить сагу
  • Сложна обработка ошибок

Оркестрация (Orchestration)

Один сервис (Orchestrator) управляет шагами саги.

// OrderSagaOrchestrator
@Service
public class OrderSagaOrchestrator {
    @Autowired
    private OrderServiceClient orderService;
    @Autowired
    private PaymentServiceClient paymentService;
    @Autowired
    private ShipmentServiceClient shipmentService;
    
    @Transactional
    public void executeOrderSaga(OrderRequest request) {
        try {
            // Шаг 1: Создать заказ
            Long orderId = orderService.createOrder(request).getId();
            
            // Шаг 2: Обработать платёж
            try {
                paymentService.processPayment(orderId, request.getAmount());
            } catch (PaymentException e) {
                // Откатить заказ
                orderService.cancelOrder(orderId);
                throw new OrderSagaException("Payment failed", e);
            }
            
            // Шаг 3: Подготовить доставку
            try {
                shipmentService.prepareShipment(orderId);
            } catch (ShipmentException e) {
                // Откатить платёж и заказ
                paymentService.refund(orderId);
                orderService.cancelOrder(orderId);
                throw new OrderSagaException("Shipment failed", e);
            }
            
        } catch (Exception e) {
            // Обработка ошибки
            logger.error("Order saga failed", e);
            throw e;
        }
    }
}

Преимущества:

  • Проще логика
  • Лучше контроль потока

Недостатки:

  • Tight coupling
  • Single point of failure

2. Event Sourcing + Compensation

Хранить все события, использовать компенсирующие транзакции для отката.

// Событие платежа
@Entity
public class PaymentEvent {
    @Id
    private Long id;
    
    private Long orderId;
    private BigDecimal amount;
    private PaymentStatus status;  // PENDING, COMPLETED, COMPENSATED
    private LocalDateTime timestamp;
    private String eventType;  // PAYMENT_CREATED, PAYMENT_REFUNDED
}

@Service
public class PaymentEventStore {
    @Autowired
    private PaymentEventRepository eventRepository;
    
    @Transactional
    public void recordPayment(Long orderId, BigDecimal amount) {
        PaymentEvent event = new PaymentEvent();
        event.setOrderId(orderId);
        event.setAmount(amount);
        event.setStatus(PaymentStatus.COMPLETED);
        event.setEventType("PAYMENT_CREATED");
        event.setTimestamp(LocalDateTime.now());
        
        eventRepository.save(event);
    }
    
    @Transactional
    public void compensatePayment(Long orderId) {
        PaymentEvent event = new PaymentEvent();
        event.setOrderId(orderId);
        event.setStatus(PaymentStatus.COMPENSATED);
        event.setEventType("PAYMENT_REFUNDED");
        event.setTimestamp(LocalDateTime.now());
        
        eventRepository.save(event);  // Регистрирует компенсацию
        // Фактически обработать возврат
        processRefund(orderId);
    }
}

3. Two-Phase Commit (2PC) — Избегать!

В распределённых системах 2PC работает плохо, но для критичных операций иногда используется.

// Координатор
@Service
public class TransactionCoordinator {
    @Autowired
    private PaymentServiceClient paymentService;
    @Autowired
    private InventoryServiceClient inventoryService;
    
    public void executeTransaction(Long orderId, BigDecimal amount) {
        // Phase 1: Prepare
        boolean paymentOk = paymentService.preparePayment(orderId, amount);
        boolean inventoryOk = inventoryService.reserveInventory(orderId);
        
        if (paymentOk && inventoryOk) {
            // Phase 2: Commit
            paymentService.commitPayment(orderId);
            inventoryService.commitReservation(orderId);
        } else {
            // Rollback
            paymentService.abortPayment(orderId);
            inventoryService.abortReservation(orderId);
        }
    }
}

Это не рекомендуется — может привести к deadlock-ам и блокировкам.

4. Идемпотентность + Retry

Сделать операции идемпотентными, чтобы можно было безопасно повторять.

@Service
public class PaymentService {
    @Autowired
    private PaymentRepository paymentRepository;
    @Autowired
    private BankAPI bankAPI;
    
    @Transactional
    public PaymentResponse processPayment(String idempotencyKey, BigDecimal amount) {
        // Проверить если платёж уже обработан
        Optional<Payment> existing = paymentRepository.findByIdempotencyKey(idempotencyKey);
        if (existing.isPresent()) {
            return existing.get().toResponse();  // Вернуть тот же результат
        }
        
        // Обработать платёж
        PaymentResponse response = bankAPI.charge(amount);
        
        // Сохранить результат с idempotencyKey
        Payment payment = new Payment();
        payment.setIdempotencyKey(idempotencyKey);
        payment.setAmount(amount);
        payment.setStatus(response.getStatus());
        paymentRepository.save(payment);
        
        return response;
    }
}

// Клиент с retry
@Service
public class OrderClient {
    @Autowired
    private RestTemplate restTemplate;
    
    public void processPaymentWithRetry(String idempotencyKey, BigDecimal amount) {
        Retry retry = Retry.of("payment", RetryConfig.custom()
            .maxAttempts(3)
            .waitDuration(Duration.ofSeconds(1))
            .build());
        
        Retry.executeFunction(retry, () -> {
            paymentService.processPayment(idempotencyKey, amount);
            return null;
        });
    }
}

5. Компенсирующие операции

Для каждого действия определить компенсацию.

// Действие: Зарезервировать товар
public class ReserveInventoryCommand {
    private Long orderId;
    private List<OrderItem> items;
}

// Компенсация: Освободить резервацию
public class ReleaseInventoryCommand {
    private Long orderId;
    private List<OrderItem> items;
}

@Service
public class OrderSaga {
    @Autowired
    private CommandBus commandBus;
    
    public void handleOrderCreated(OrderCreatedEvent event) {
        try {
            // Шаг 1
            commandBus.send(new ReserveInventoryCommand(event.getOrderId(), event.getItems()));
            
            // Шаг 2
            commandBus.send(new ProcessPaymentCommand(event.getOrderId()));
            
        } catch (Exception e) {
            // Откатить в обратном порядке
            commandBus.send(new ReleaseInventoryCommand(event.getOrderId(), event.getItems()));
            commandBus.send(new RefundPaymentCommand(event.getOrderId()));
        }
    }
}

Лучшие практики

  1. Используй Saga паттерн для распределённых операций
  2. Предпочитай хореографию для простых сценариев
  3. Используй оркестрацию для сложных саг
  4. Сделай операции идемпотентными — ключ к надежности
  5. Используй Idempotency-Key для обнаружения дубликатов
  6. Логируй все события для отладки
  7. Избегай 2PC в продакшене
  8. Определи компенсирующие операции для каждого шага
  9. Тестируй сценарии отката в интеграционных тестах
  10. Мониторь саги на предмет зависаний