Ответ
В микросервисной архитектуре классические ACID-транзакции, охватывающие несколько сервисов, практически невозможны из-за необходимости блокировок и сильной связанности. Вместо них применяется модель конечной консистентности (Eventual Consistency), которая достигается с помощью паттернов.
Основной паттерн для управления распределенными транзакциями — Saga.
Паттерн Saga
Saga — это последовательность локальных транзакций. Каждый шаг саги обновляет данные в одном сервисе и публикует событие, которое запускает следующий шаг. Если какой-либо шаг завершается неудачей, сага выполняет компенсирующие транзакции в обратном порядке, чтобы отменить уже выполненные шаги.
Существует два способа координации саги:
Хореография (Choreography): Сервисы общаются напрямую друг с другом через события. Один сервис выполняет свою транзакцию и публикует событие в брокер сообщений. Другие сервисы подписываются на это событие, выполняют свои транзакции и публикуют новые события.
- Плюсы: Простота, отсутствие единой точки отказа.
- Минусы: Сложно отслеживать полный флоу транзакции, риск циклических зависимостей.
Оркестрация (Orchestration): Центральный сервис-оркестратор управляет всем процессом. Он говорит каждому участнику, какую локальную транзакцию выполнить. Если что-то идет не так, оркестратор отвечает за запуск компенсирующих транзакций.
- Плюсы: Централизованная логика, ясный флоу, проще в отладке.
- Минусы: Дополнительный сервис, который может стать единой точкой отказа.
Концептуальный пример оркестратора на Go:
type Step struct {
Execute func(ctx context.Context) error
Compensate func(ctx context.Context) error
}
// runSaga выполняет шаги и откатывается при ошибке.
func runSaga(ctx context.Context, steps []Step) error {
completedSteps := 0
for _, step := range steps {
if err := step.Execute(ctx); err != nil {
// Ошибка на текущем шаге, запускаем компенсацию
for i := completedSteps - 1; i >= 0; i-- {
// Логгируем ошибки компенсации, но продолжаем откат
if compErr := steps[i].Compensate(ctx); compErr != nil {
log.Printf("Saga compensation for step %d failed: %v", i, compErr)
}
}
return fmt.Errorf("saga execution failed: %w", err)
}
completedSteps++
}
return nil
}
Другие важные паттерны:
- Transactional Outbox: Гарантирует, что событие будет отправлено хотя бы один раз после успешного коммита транзакции в БД (см. связанный вопрос).
- Двухфазный коммит (2PC): Классический, но редко используемый в микросервисах паттерн из-за его блокирующей природы и чувствительности к сбоям координатора.
Ключевое требование для всех этих паттернов — идемпотентность операций. Обработчики событий должны быть спроектированы так, чтобы повторное получение одного и того же сообщения не приводило к дублированию данных или побочным эффектам.