Система распределённых транзакций: 2PC и Saga в проектировании

Сегодня разбираем материал dev.to о теме «Распределённые транзакции в проектировании систем: 2PC и Saga». Материал полезен тем, кто хочет быстро понять суть темы и перевести идеи в прикладные действия.


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

Что такое распределённые транзакции

Распределённая транзакция включает несколько ресурсов, таких как базы данных, микросервисы или внешние системы, которые должны работать в унисон для достижения общего результата. Это отличается от локальных транзакций, которые касаются только одного ресурса и требуют согласованности между сервисами, особенно в условиях сетевых задержек и сбоев

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

Поэтому проектировщики систем должны находить средства, которые обеспечивают баланс между строгой согласованностью и доступностью.

Протокол двухфазной фиксации (2PC)

Протокол двухфазной фиксации (Two-Phase Commit, 2PC) — классическое решение для достижения строгой согласованности в распределённых транзакциях. Введённый в 1970-х годах, 2PC опирается на центральный координатор и множество участников для обеспечения семантики «всё или ничего» при работе с разнородными ресурсами.

Основные компоненты 2PC

Координатор — центральный орган, ответственный за управление транзакцией. Он получает исходный запрос транзакции и управляет процессом голосования и принятия решений.

Участники — отдельные ресурсы (базы данных или сервисы), которые выполняют локальную работу и реагируют на инструкции координатора.

Менеджер транзакций — часть, которая часто реализуется с использованием стандартов, таких как XA (eXtended Architecture, расширенная архитектура), для взаимодействия с базами данных.

Фазы протокола 2PC

2PC работает в двух отдельных фазах, обеспечивая безопасность до того, как произойдут какие-либо постоянные изменения.

Фаза подготовки (фаза голосования): координатор отправляет сообщение prepare всем участникам. Каждый участник выполняет необходимые локальные операции, захватывает блокировки, записывает изменения в долговечный журнал и отвечает либо ready (голос «да»), либо abort (голос «нет»). Если какой-либо участник голосует «нет» или не отвечает, координатор принимает решение об отмене.

Если все участники проголосовали «да», координатор записывает глобальное решение о фиксации и рассылает сообщения о фиксации каждому участнику. Учтите, что если решение об отмене, координатор отправляет сообщения о возврате, и участники отменяют свои изменения, используя подготовленный журнал.

Псевдокод реализации координатора 2PC

class TwoPhaseCommitCoordinator { List participants; TransactionLog log;
void beginTransaction(Transaction tx) { log.write("BEGIN_TX", tx.id); boolean allReady = true;
// Prepare Phase for each participant in participants { Response response = participant.prepare(tx); if (!response.isReady()) { allReady = false; break; } }
// Decision if (allReady) { log.write("GLOBAL_COMMIT", tx.id); for each participant in participants { participant.commit(tx); } } else { log.write("GLOBAL_ABORT", tx.id); for each participant in participants { participant.rollback(tx); } } }
}

Псевдокод участника 2PC

class DatabaseParticipant implements Participant { LocalDatabase db; UndoLog undoLog;
Response prepare(Transaction tx) { try { db.acquireLocks(tx.operations); db.executeOperations(tx.operations); // tentative changes undoLog.recordUndoInfo(tx); return new Response(true, "READY"); } catch (Exception e) { return new Response(false, "ABORT"); } }
void commit(Transaction tx) { db.makeChangesPermanent(tx); db.releaseLocks(tx); undoLog.clear(tx); }
void rollback(Transaction tx) { db.applyUndo(undoLog.getUndoInfo(tx)); db.releaseLocks(tx); undoLog.clear(tx); }
}

Эти структуры кода иллюстрируют блокирующую природу 2PC: участники удерживают блокировки с фазы подготовки до поступления окончательного решения. Координатор обязан сохранить своё решение долговечным образом перед продолжением — это обеспечивает восстанавливаемость после сбоев.

Ограничения 2PC

Хотя 2PC гарантирует строгую согласованность, он страдает от нескольких критических недостатков. Координатор становится единой точкой отказа и узким местом производительности. Протокол является блокирующим: если координатор выходит из строя после фазы подготовки, участники остаются заблокированными на неопределённый срок до восстановления. Сетевые разделения способны вызвать длительную недоступность. В высоконагруженных средах микросервисов эти проблемы делают 2PC непрактичным для длительных операций

На практике я видел, как команды недооценивают этот риск и получают каскадные блокировки в продакшене именно из-за сбоя координатора в неподходящий момент.

Паттерн Saga

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

Ключевые принципы Saga

Локальные транзакции: каждый сервис выполняет свою часть независимо и фиксирует изменения немедленно.

Компенсирующие транзакции: обратимые операции, которые восстанавливают систему в согласованное состояние без глобального отката.

Отсутствие глобальной блокировки: ресурсы остаются доступными на протяжении всего процесса.

Итоговая согласованность: система приходит к согласованному состоянию со временем, а не мгновенно.

Два стиля реализации Saga

Хореографическая Saga (Choreography-based Saga): сервисы взаимодействуют напрямую через события. Каждый сервис прослушивает события от предыдущих шагов и публикует собственные события по завершении или при сбое. Центральный контроллер отсутствует. Этот стиль способствует слабой связанности, но может стать трудным для отслеживания по мере роста числа сервисов.

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

Полный пример оркестрационной Saga: обработка заказа в интернет-магазине

Рассмотрим интернет-магазин, где размещение заказа включает три сервиса: Order Service, Payment Service и Inventory Service. Saga гарантирует, что если платёж не прошёл, запасы не будут списаны, или если товар недоступен, платёж будет возвращён.

class OrderSagaOrchestrator { OrderService orderService; PaymentService paymentService; InventoryService inventoryService; SagaStateRepository stateRepo;
void startOrderSaga(OrderRequest request) { SagaInstance saga = new SagaInstance(request.orderId); stateRepo.save(saga);
// Step 1: Create Order (local transaction) Order order = orderService.createOrder(request); saga.updateStep("ORDER_CREATED", order);
try { // Step 2: Process Payment Payment payment = paymentService.processPayment(order); saga.updateStep("PAYMENT_SUCCESS", payment);
// Step 3: Reserve Inventory InventoryReservation reservation = inventoryService.reserveInventory(order); saga.updateStep("INVENTORY_RESERVED", reservation);
saga.complete(); return;
} catch (PaymentFailedException e) { // Compensation: Cancel Order orderService.cancelOrder(order); saga.fail("PAYMENT_FAILED");
} catch (InventoryUnavailableException e) { // Compensation Chain paymentService.refundPayment(payment); orderService.cancelOrder(order); saga.fail("INVENTORY_FAILED"); } }
// Compensating transaction examples void compensatePayment(Payment payment) { paymentService.refundPayment(payment); // idempotent refund }
void compensateOrder(Order order) { orderService.cancelOrder(order); // releases any reservations }
}

Пример локальной транзакции на уровне сервиса (Inventory Service):

class InventoryService { InventoryRepository repo;
InventoryReservation reserveInventory(Order order) { // Local transaction - fully committed immediately return repo.withinTransaction(() -> { Stock stock = repo.findStock(order.productId); if (stock.quantity < order.quantity) { throw new InventoryUnavailableException(); } stock.quantity -= order.quantity; repo.save(stock); return new InventoryReservation(order.orderId, order.quantity); }); }
// Compensating transaction - public and idempotent void releaseInventory(InventoryReservation reservation) { repo.withinTransaction(() -> { Stock stock = repo.findStock(reservation.productId); stock.quantity += reservation.quantity; repo.save(stock); }); }
}

Эта полная структура кода демонстрирует, как оркестратор управляет Saga, в то время как каждый сервис остаётся ответственным только за свою локальную ACID-транзакцию и свою компенсирующую транзакцию. Ключи идемпотентности должны быть включены в каждую команду и компенсацию для безопасной обработки повторных попыток после сетевых сбоев

Преимущества паттерна Saga

Saga превосходит другие подходы в микросервисах, поскольку избегает длительно удерживаемых блокировок, повышает доступность и масштабируется горизонтально. Сбои запускают целевые компенсации, а не глобальные откаты. Паттерн естественно вписывается в событийно-ориентированные архитектуры и работает без проблем с очередями сообщений, такими как Kafka или RabbitMQ, для надёжной доставки событий.

Выбор между 2PC и Saga

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

На практике многие проекты систем сочетают оба подхода: 2PC для критических синхронных шагов внутри ограниченного контекста (bounded context) и Saga для оркестрации между контекстами. Я рекомендую начинать с Saga в новых микросервисных проектах и добавлять 2PC точечно там, где бизнес-требования к согласованности не оставляют выбора.

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

Типичные ошибки при реализации распределённых транзакций

Даже хорошо спроектированная архитектура ломается, если упустить детали реализации. На моём опыте именно следующие ошибки встречаются чаще всего.

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

Отсутствие мониторинга экземпляров Saga. Зависшие Saga-процессы — реальная проблема в продакшене. Без явного отслеживания состояния каждого экземпляра и алертов на зависшие шаги система накапливает незавершённые транзакции, которые трудно обнаружить.

Игнорирование частичных сбоев в 2PC. Если координатор падает после записи GLOBAL_COMMIT, но до отправки сообщений участникам, часть из них зафиксирует изменения, а часть — нет. Без механизма восстановления из журнала это приводит к несогласованности данных.

Смешение стилей Saga без явной причины. Хореография удобна для простых линейных потоков, оркестрация — для сложных с ветвлением и компенсациями. Смешение обоих стилей в одном процессе без чёткой логики усложняет отладку и мониторинг.

Отсутствие таймаутов и политик повтора. Распределённые вызовы без таймаутов блокируют потоки и ресурсы. Каждый шаг Saga и каждый вызов участника в 2PC должен иметь явный таймаут и политику повтора с экспоненциальной задержкой.

Распределённые транзакции (2PC, Saga) в проектировании систем остаются краеугольной темой, которую каждый профессиональный системный архитектор должен освоить для создания надёжных, масштабируемых и поддерживаемых крупномасштабных приложений.

Ответы на эти вопросы могут быть для вас полезными

Когда 2PC предпочтительнее Saga? 2PC оправдан там, где бизнес-требования не допускают даже временной несогласованности: банковские переводы, финансовые расчёты, операции с критическими данными внутри одного ограниченного контекста. Если допустима итоговая согласованность — выбирайте Saga.

Что такое компенсирующая транзакция и чем она отличается от отката? Компенсирующая транзакция — это отдельная бизнес-операция, которая логически отменяет эффект уже зафиксированного шага. В отличие от отката (rollback) в классическом смысле, она не отменяет изменения на уровне базы данных, а создаёт новую запись, нейтрализующую предыдущую. Например, вместо отмены списания средств выполняется возврат.

Как обеспечить идемпотентность компенсаций в Saga? Каждой команде и компенсации присваивается уникальный ключ идемпотентности (idempotency key). Перед выполнением сервис проверяет, не была ли операция с таким ключом уже выполнена. Если да — возвращает предыдущий результат без повторного выполнения.

Можно ли использовать Kafka или RabbitMQ для реализации хореографической Saga? Да, это распространённая практика. Kafka и RabbitMQ обеспечивают надёжную доставку событий между сервисами. Kafka удобен для высоконагруженных систем с необходимостью воспроизведения событий, RabbitMQ — для более простых сценариев с маршрутизацией сообщений. Оба инструмента поддерживают семантику «хотя бы один раз», что делает идемпотентность обработчиков обязательным требованием.

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

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x