📜 Паттерн Saga: Как откатить то, что откатить нельзя — 11 июня 2026 г. в 05:57:40.816
📜 Паттерн Saga: Как откатить то, что откатить нельзя Представьте классическую задачу: клиент нажимает кнопку «Купить тур». Вашему бэкенду нужно сделать три вещи: 1. Забронировать рейс (через API авиакомпании). 2. Забронировать отель (через микросервис отелей). 3. Списать деньги (через платежный шлюз). В монолите с одной базой вы бы просто открыли транзакцию BEGIN ... COMMIT и спали спокойно. Но у нас микросервисы. Вы не можете повесить лок на базу данных авиакомпании. Что если рейс забронирован, отель подтвержден, а на карте клиента нет денег? Вы не можете просто сказать ROLLBACK. Рейс уже куплен. Вы потеряли деньги компании. Здесь на сцену выходит Паттерн Saga. Суть Саги: Распределенная транзакция разбивается на серию локальных. И самое главное: для каждого шага (Execute) вы обязаны написать шаг отмены (Compensate). Если на третьем шаге происходит ошибка, Сага разворачивается и вызывает функции отмены для второго и первого шагов в обратном порядке. В архитектуре есть два пути: Хореография (микросервисы кидаются событиями через Kafka) и Оркестрация (один сервис управляет всем). В Go элегантнее всего пишется Оркестратор. Реализация Оркестратора на Go: type SagaStep struct { Name string Execute func(ctx context.Context) error Compensate func(ctx context.Context) error } func RunSaga(ctx context.Context, steps []SagaStep) error { var completedSteps []SagaStep // 1. Идем вперед for _, step := range steps { err := step.Execute(ctx) if err == nil { completedSteps = append(completedSteps, step) continue } log.Printf("Ошибка на шаге %s: %v. Начинаем откат...", step.Name, err) // 2. Ошибка! Идем назад и откатываем for i := len(completedSteps) - 1; i >= 0; i-- { rollbackStep := completedSteps[i] if compErr := rollbackStep.Compensate(ctx); compErr != nil { // Это катастрофа. Подробнее в Senior Tips. log.Printf("КРИТИЧЕСКАЯ ОШИБКА отката %s: %v", rollbackStep.Name, compErr) } } return fmt.Errorf("saga failed on step %s: %w", step.Name, err) } return nil } Как это выглядит при вызове: steps := []SagaStep{ { Name: "BookFlight", Execute: func(ctx context.Context) error { return flightAPI.Book(userID) }, Compensate: func(ctx context.Context) error { return flightAPI.Cancel(userID) }, }, { Name: "BookHotel", Execute: func(ctx context.Context) error { return hotelAPI.Book(userID) }, Compensate: func(ctx context.Context) error { return hotelAPI.Cancel(userID) }, }, // ... и так далее } err := RunSaga(ctx, steps) 🔥 Нюансы: 1. Компенсации не имеют права падать. Если функция Execute упала, это нормально (нет денег на карте, сеть моргнула). Но если упала функция Compensate (не смогли отменить бронь отеля) - ваша система переходит в неконсистентное состояние. Отель ждет гостя, а клиент ничего не оплатил. Решение: Компенсирующие действия должны отправляться в надежную очередь (Dead Letter Queue) и ретраиться до посинения (или до вмешательства саппорта). 2. Идемпотентность - царица Саги. Так как функции компенсации могут ретраиться бесконечно, они обязаны быть идемпотентными (вспоминаем прошлый пост!). Если мы 5 раз вызовем hotelAPI.Cancel, бронь должна отмениться один раз, а остальные 4 вызова должны вернуть 200 OK. 3. Грязное чтение (Lack of Isolation). Сага не обладает свойством "I" из ACID (Изоляция). Между бронированием отеля и списанием денег могут пройти секунды. В этот момент другой сервис может увидеть, что отель забронирован. Если сага откатится, другой сервис останется с неверными данными. Учитывайте это при проектировании (например, вводите статусы PENDING). Сага - это сложно. Не тяните её в проект, пока можете обойтись одной таблицей в монолите. Но если уж распределили базу, будьте готовы платить за это кодом. 👉 @golang_lib

