Что такое паттерн Saga и как он применяется в распределенных системах?

Ответ

Saga — это паттерн управления транзакциями в распределенных системах (например, в микросервисной архитектуре), который помогает поддерживать целостность данных без использования традиционных ACID-транзакций и блокировок.

Вместо одной большой транзакции, Saga разбивает бизнес-процесс на последовательность локальных транзакций. Каждая из этих транзакций атомарна и выполняется в рамках одного сервиса.

Ключевой принцип: Если какой-либо шаг в последовательности завершается неудачно, Saga запускает компенсирующие транзакции в обратном порядке для отмены уже выполненных шагов. Компенсирующая транзакция — это операция, которая логически отменяет действие предыдущей транзакции (например, возврат средств вместо отмены платежа).

Способы реализации:

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

    • Сервисы общаются друг с другом через асинхронные события (events).
    • Каждый сервис после выполнения своей части работы публикует событие. Другие сервисы подписываются на эти события и запускают свои локальные транзакции.
    • Плюсы: Слабая связанность сервисов.
    • Минусы: Сложно отслеживать общий статус процесса, так как нет центральной точки управления.
  2. Оркестрация (Orchestration):

    • Существует центральный сервис-координатор (оркестратор), который управляет всем процессом.
    • Оркестратор последовательно вызывает нужные сервисы для выполнения локальных транзакций. В случае сбоя он же отвечает за вызов компенсирующих транзакций.
    • Плюсы: Централизованная логика, проще понимать и отлаживать процесс.
    • Минусы: Оркестратор может стать единой точкой отказа (Single Point of Failure).

Пример концепции на Go (Оркестрация):

// Каждый шаг — это вызов отдельного микросервиса
func placeOrderSaga(orderID string, userID string, money float64) error {
    // Шаг 1: Зарезервировать товары
    if err := inventoryService.ReserveItems(orderID); err != nil {
        return fmt.Errorf("ошибка резервации: %w", err)
    }

    // Шаг 2: Списать средства с клиента
    if err := paymentService.Charge(userID, money); err != nil {
        // Компенсация для шага 1
        inventoryService.CancelReservation(orderID)
        return fmt.Errorf("ошибка оплаты: %w", err)
    }

    // Шаг 3: Создать доставку
    if err := shippingService.CreateShipment(orderID); err != nil {
        // Компенсация для шага 2 и 1
        paymentService.Refund(userID, money)
        inventoryService.CancelReservation(orderID)
        return fmt.Errorf("ошибка доставки: %w", err)
    }

    return nil
}

Ответ 18+ 🔞

Давай разжую тебе эту тему про Saga, а то я вижу, у тебя глаза уже стекленеют, как у дохлой рыбы. Сидишь и думаешь: "Ну вот, опять эти ваши транзакции, ACID, микросервисы... Ёпта, дайте мне просто код написать!".

А суть-то проще, чем кажется, блядь. Представь, что ты заказываешь пиццу, сука. Это же не один процесс, а цепочка, ёпта!

  1. Ты звонишь в пиццерию — это шаг "Создать заказ".
  2. Тебе говорят: "Деньги на карте есть?" — это шаг "Списать бабки".
  3. Повар начинает готовить — это шаг "Зарезервировать ингредиенты".
  4. Курьер выезжает — это шаг "Создать доставку".

И вот представь, хули может пойти не так? Курьер, сука, упал с самоката и разбил все коробки. Пиздец, заказ не выполнен. Что делать? По классическим ACID-транзакциям мы бы откатили ВСЁ: и списание денег, и резервацию ингредиентов, и запись в базе. Но в микросервисах так не выйтет, блядь! У каждого сервиса своя база, своя юрисдикция, своя жопа.

Вот тут и появляется наша Saga, как хитрая жопа, которая всех прикроет. Её философия проста: если где-то посерешь — убери за собой, сука.

Вместо одной жирной транзакции, мы делаем кучу маленьких, но каждая из них — атомарная в своём сервисе. А главный принцип — компенсация. Не получился следующий шаг? Начинаем откатывать всё в обратном порядке, как мартышка по лианам, только с криками "А-а-а, блядь!".

  • Курьер разбился? Отменяем доставку.
  • Деньги уже списали? Возвращаем на карту, ёбана.
  • Ингредиенты зарезервировали? Отменяем резерв, пусть другие пиццы готовят.

И есть два главных способа эту кашу организовать, прямо как в жизни.

1. Хореография (Choreography) Это когда все сервисы — как пьяные гости на хате. Никто никого не контролирует, но все что-то делают по цепочке событий.

  • Сервис заказа кричит: "Я заказ создал, ёпта!".
  • Сервис оплаты слышит это, списывает деньги и орёт: "Бабок нет, всё спиздил!".
  • Сервис кухни слышит это и начинает готовить.
  • Если на каком-то этапе тишина — значит, всё пошло по пизде, и нужно начинать орать в обратном порядке: "Отмена!".

Плюсы: Все независимы, нет главного пахана. Минусы: Пиздец как сложно понять, кто сейчас в каком состоянии, если вся цепочка из двадцати сервисов. Кто виноват, что пицца не едет? Хуй поймёшь, все друг на друга пальцем тычут.

2. Оркестрация (Orchestration) А вот это уже по-взрослому. Появляется главный пахан — сервис-оркестратор. Он, как режиссёр-постановщик, командует:

  • Ты — списывай деньги.
  • Ты — готовь пиццу.
  • Ты — вези. И если курьер накосячил, оркестратор сам знает, кого бить по ебалу и кому кричать "Откатывай!".

Плюсы: Всё ясно, всё под контролем, логика в одном месте. Минусы: Если этот главный пахан-оркестратор ляжет с инфарктом (упадёт), то вся система встанет, блядь. Единая точка отказа, ёпта.

Вот тебе пример на Go, как это выглядит в коде, если делать через оркестрацию. Смотри, не моргай.

// Каждый шаг — это вызов отдельного микросервиса
func placeOrderSaga(orderID string, userID string, money float64) error {
    // Шаг 1: Зарезервировать товары
    if err := inventoryService.ReserveItems(orderID); err != nil {
        return fmt.Errorf("ошибка резервации: %w", err)
    }

    // Шаг 2: Списать средства с клиента
    if err := paymentService.Charge(userID, money); err != nil {
        // Компенсация для шага 1
        inventoryService.CancelReservation(orderID)
        return fmt.Errorf("ошибка оплаты: %w", err)
    }

    // Шаг 3: Создать доставку
    if err := shippingService.CreateShipment(orderID); err != nil {
        // Компенсация для шага 2 и 1
        paymentService.Refund(userID, money)
        inventoryService.CancelReservation(orderID)
        return fmt.Errorf("ошибка доставки: %w", err)
    }

    return nil
}

Видишь эту красоту? Всё по полочкам. Не получилось списать деньги — отменяем резерв. Не получилось создать доставку — хуяк, возвращаем деньги И отменяем резерв. Всё логично, всё по-честному.

Вот и вся магия, блядь. Не транзакция, а скорее протокол приличия для микросервисов: "Накосячил — исправь, мудак".