Gymterview
senior

Что такое паттерн 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 (двухфазным коммитом).