Что такое паттерн Saga и как он решает проблему распределённых транзакций?
Saga — это паттерн управления распределёнными транзакциями, при котором длинная бизнес-транзакция разбивается на последовательность локальных транзакций в разных сервисах. При сбое выполняются компенсирующие транзакции для отмены уже выполненных шагов.
Аналогия из жизни: бронирование путешествия. Вы бронируете авиабилет, затем отель, затем экскурсию. Если экскурсию забронировать не удалось — вы отменяете отель и авиабилет (компенсирующие действия).
Проблема
В микросервисах нельзя использовать распределённые ACID-транзакции (2PC — Two-Phase Commit), так как они плохо масштабируются и создают сильную связность.
Два подхода к реализации
| Критерий | Хореография | Оркестрация |
|---|---|---|
| Координация | Каждый сервис сам публикует/слушает события | Центральный оркестратор управляет шагами |
| Связность | Сервисы слабо связаны | Оркестратор знает обо всех шагах |
| Единая точка отказа | Нет | Оркестратор |
| Видимость процесса | Сложно отследить | Легко |
| Рекомендации | 3-4 шага | 5+ шагов |
Хореография (Choreography)
// Сервис заявок — публикует событие
@Service
public class ApplicationService {
@Transactional
public void createApplication(CreditApplication app) {
applicationRepository.save(app);
eventPublisher.publish(new ApplicationCreatedEvent(app.getId(), app.getCustomerId()));
}
// Компенсация
@KafkaListener(topics = "scoring-failed-events")
public void handleScoringFailed(ScoringFailedEvent event) {
applicationRepository.updateStatus(event.getApplicationId(), Status.REJECTED);
}
}
// Сервис скоринга — слушает событие и обрабатывает
@Service
public class ScoringService {
@KafkaListener(topics = "application-created-events")
public void handleApplicationCreated(ApplicationCreatedEvent event) {
ScoringResult result = performScoring(event.getCustomerId());
if (result.isApproved()) {
eventPublisher.publish(new ScoringApprovedEvent(event.getApplicationId()));
} else {
eventPublisher.publish(new ScoringFailedEvent(event.getApplicationId()));
}
}
}
Оркестрация (Orchestration)
// Saga-оркестратор
@Service
@RequiredArgsConstructor
public class CreditSagaOrchestrator {
private final ApplicationClient applicationClient;
private final ScoringClient scoringClient;
private final AccountClient accountClient;
public void executeCreditSaga(CreditRequest request) {
SagaState state = new SagaState(request);
try {
// Шаг 1: Создать заявку
state.setApplicationId(applicationClient.create(request));
// Шаг 2: Скоринг
ScoringResult scoring = scoringClient.evaluate(request.getCustomerId());
if (!scoring.isApproved()) {
throw new ScoringRejectedException(scoring.getReason());
}
// Шаг 3: Открыть счёт
state.setAccountId(accountClient.openCreditAccount(request));
// Шаг 4: Перевести средства
paymentClient.transfer(state.getAccountId(), request.getAmount());
} catch (Exception e) {
compensate(state);
throw new SagaFailedException("Кредитная saga провалена", e);
}
}
private void compensate(SagaState state) {
if (state.getAccountId() != null) {
accountClient.closeAccount(state.getAccountId());
}
if (state.getApplicationId() != null) {
applicationClient.reject(state.getApplicationId());
}
}
}
На собеседовании: знайте оба подхода и когда какой выбрать. Обязательно упомяните компенсирующие транзакции — это ядро паттерна. Частая ошибка — путать Saga с 2PC (двухфазным коммитом).