Как обеспечить консистентность данных в распределенной системе (микросервисная архитектура)?

Ответ

В микросервисной архитектуре классические ACID-транзакции, охватывающие несколько сервисов, не применяются из-за сильной связанности и проблем с производительностью. Вместо этого достигается конечная согласованность (eventual consistency) с помощью паттернов распределенных транзакций.

Основной паттерн — Saga.

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

Существует два способа координации саги:

  1. Хореография (Choreography): сервисы общаются напрямую через события, без центрального координатора. Это обеспечивает слабую связанность.

    Пример создания заказа:

    // OrderService
    func (s *Service) CreateOrder(orderData) error {
        // 1. Локальная транзакция: сохранить заказ в статусе "pending"
        if err := s.db.SaveOrderAsPending(orderData); err != nil {
            return err
        }
        // 2. Публикация события для следующего шага
        return s.eventBus.Publish("OrderCreated", orderData)
    }
    
    // PaymentService (подписчик на "OrderCreated")
    func (s *Service) OnOrderCreated(event) error {
        if err := s.db.ChargePayment(); err != nil {
            // Публикуем компенсирующее событие
            s.eventBus.Publish("PaymentFailed", event.OrderID)
            return err
        }
        // Публикуем событие об успешной оплате
        s.eventBus.Publish("PaymentSucceeded", event.OrderID)
    }
  2. Оркестрация (Orchestration): центральный сервис (оркестратор) управляет всем процессом, отправляя команды каждому участнику и ожидая ответа.

Связанные паттерны:

  • Outbox Pattern: Гарантирует, что событие будет отправлено тогда и только тогда, когда локальная транзакция успешно завершена. Изменения и события записываются в одну транзакционную таблицу "outbox", а отдельный процесс считывает ее и надежно доставляет сообщения.
  • CQRS (Command Query Responsibility Segregation): Часто используется вместе с сагами. Разделение моделей на запись (Commands) и чтение (Queries) упрощает обработку событий и построение согласованных представлений данных.

Ответ 18+ 🔞

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

Поэтому тут главный принцип — конечная согласованность, блядь. То есть, в итоге-то всё сойдётся, но не сразу, а когда-нибудь потом, как зарплата у госслужащего. А основной инструмент для этого — Saga, сука.

Представь себе сагу — это как длинная, ебать, история, где каждый сервис делает своё маленькое дело (локальную транзакцию), а потом кричит в рупор (публикует событие): «Эй, следующий, твой выход!». Если следующий чувак споткнётся и упадёт мордой в асфальт, то придётся бегать назад и откатывать всё, что натворили предыдущие, с помощью компенсирующих транзакций. Весело, да?

И есть два способа эту кашу заварить:

  1. Хореография (Choreography). Это когда все сервисы, как пьяные гости на свадьбе, сами между собой договариваются, кто за кем идёт. Центрального заводилы нет, слабая связанность, красота. Один сделал дело — крикнул, другой услышал — сделал своё.

    Вот смотри, как заказ создаётся, на примере кода (его не трогаем, он святой):

    // OrderService
    func (s *Service) CreateOrder(orderData) error {
        // 1. Локально в своей базе заказ сохранил, статус "pending"
        if err := s.db.SaveOrderAsPending(orderData); err != nil {
            return err
        }
        // 2. Орёшь на всю площадку: "Заказ создан, ёбта!"
        return s.eventBus.Publish("OrderCreated", orderData)
    }
    
    // PaymentService (сидит, ушами шевелит, слушает)
    func (s *Service) OnOrderCreated(event) error {
        // Пытается деньги списать
        if err := s.db.ChargePayment(); err != nil {
            // Не получилось! Орёт: "Ой, всё, платеж не прошёл, отменяйте!"
            s.eventBus.Publish("PaymentFailed", event.OrderID)
            return err
        }
        // Получилось! Орёт: "Деньги есть, работаем дальше!"
        s.eventBus.Publish("PaymentSucceeded", event.OrderID)
    }
  2. Оркестрация (Orchestration). А это когда есть главный по тарелочкам — оркестратор. Он, как злой режиссёр, тыкает пальцем в каждого сервиса: «Ты — делай! Ты — теперь ты! А ты — откатывай, потому что этот мудак всё просрал!». Все команды идут через него одного.

Паттерны-помощники, без которых нихуя не работает:

  • Outbox Pattern. Это чтобы событие улетело в мир ровно тогда, когда локальная транзакция завершилась, а не «ой, я записал, но сообщение потерял, извините». Записываем и данные, и событие в одну транзакцию в специальную табличку-исходящую (outbox). Потом отдельный почтальон-процесс эту табличку читает и гарантированно расталкивает сообщения всем подписчикам. Без этого — доверия ебать ноль, всё рассыпется.
  • CQRS (Command Query Responsibility Segregation). Часто с сагами в одной упряжке идёт. Смысл в том, чтобы разделить, сука, запись и чтение. Для команд (запись) — одна модель, своя база. Для запросов (чтение) — вообще другая, которая может обновляться асинхронно из событий, которые саги порождают. Так проще строить эти ваши «согласованные представления данных», которые в конечном счёте (eventual!) становятся верными. Хитрая жопа, но работает.

Вот так, блядь, и живём. Не как в старые добрые времена с одной жирной транзакцией на весь мир, а как партизаны в лесу — каждый своё, но в итоге мост всем миром взрываем.