1. Transactional Outbox. Как не потерять сообщения
  2. Two-Phased Commit и eXtended Architecture
  3. SAGA. Распределенные транзакции
  4. Try-Confirm-Cancel в распределенных системах

Переход к распределённым системам и микросервисам усилил требования к согласованности данных между независимыми сервисами.
Традиционные локальные транзакции (ACID) не масштабируются через границы сервисов, поэтому для координации изменений используются прикладные паттерны распределённых транзакций. Одним из таких паттернов является Try-Confirm-Cancel (TCC) — двухфазный подход на уровне бизнес-логики, который обеспечивает высокую степень изоляции через предварительное резервирование ресурсов и отдельную фазу окончательной фиксации или отмены.

Паттерна Try-Confirm-Cancel

Паттерн Try-Confirm-Cancel (TCC) является прикладным протоколом двухфазной фиксации, который переносит логику управления ресурсами с уровня базы данных на уровень бизнес-логики приложения. В отличие от традиционных моделей, где СУБД удерживает физические блокировки строк, TCC оперирует концепцией резервирования ресурсов. Это позволяет системе сохранять высокую производительность, так как ресурсы не блокируются на уровне системных транзакций БД, а помечаются как зарезервированные в бизнес-логике.

Модель TCC опирается на участников транзакции, которые должны поддерживать состояние резервирования до принятия окончательного решения. Весь процесс координируется инициатором или координатором транзакций (TC), который гарантирует, что все участники либо подтвердят свои резервирования, либо отменят их.

Try-Confirm-Cancel

Фаза Try: Резервирование и проверка готовности

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

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

Фаза Confirm: Окончательная фиксация бизнес-операции

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

Возвращаясь к примеру с авиабилетами, фаза Confirm переводит статус места из «зарезервировано» в «продано» и инициирует выставление счета клиенту.
Важно понимать, что на этом этапе используются только те ресурсы, которые были «заморожены» ранее. Реализация метода Confirm должна быть идемпотентной, так как из-за сетевых сбоев координатор может вызывать этот метод многократно до получения подтверждения об успехе.

Фаза Cancel: Отмена и возврат в исходное состояние

Try-Confirm-Cancel

Фаза Cancel предназначена для обработки неудачных сценариев. Если какой-либо из сервисов не смог выполнить операцию Try или если инициатор решил прервать транзакцию по иным причинам, координатор вызывает методы Cancel у всех участников, которые уже успели выполнить резервирование.

Цель этой фазы — освободить зарезервированные ресурсы и вернуть систему в состояние, максимально близкое к первоначальному. В случае с авиабилетами это означает перевод места из статуса «зарезервировано» обратно в «доступно».
Как и Confirm, операция Cancel должна быть идемпотентной и способной обрабатывать ситуации, когда запрос на отмену приходит раньше или без соответствующего запроса Try (так называемая «пустая отмена»).

Архитектурные компоненты и взаимодействие

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

Компонент Функциональная роль Ключевые действия
Инициатор транзакции Сервис, запускающий бизнес-процесс. Создает контекст транзакции через координатора, вызывает методы Try участников.
Координатор транзакций (Transaction Manager, TM) Центральный узел управления состоянием. Регистрирует участников, принимает решение о фиксации/отмене, управляет повторными попытками.
Участник (Resource Manager, RM) Сервис, управляющий конкретным ресурсом. Предоставляет интерфейсы Try, Confirm, Cancel; обеспечивает атомарность локальных изменений.

Процесс начинается с того, что инициатор вызывает TC для открытия глобальной транзакции.
Затем инициатор выполняет последовательные или параллельные вызовы POST (в REST-терминологии) к участникам для резервирования ресурсов.
Участники возвращают уникальные идентификаторы резервирования (URI), которые регистрируются в TC. На основе полученных ответов инициатор принимает решение и сообщает его TC, который берет на себя ответственность за выполнение фазы 2 через вызовы PUT (Confirm) или DELETE (Cancel).

Использование HTTP-глаголов в TCC имеет глубокий семантический смысл.
POST используется для создания ресурса-резервирования, который в случае успеха возвращает статус 201 (Created). PUT идеально подходит для подтверждения (Confirm), так как этот глагол по определению идемпотентен и используется для обновления состояния существующего ресурса. DELETE используется для отмены (Cancel), возвращая ресурс в исходное состояние или удаляя запись о резервировании.

Сравнительный анализ: TCC, Saga и 2PC

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

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

Протокол 2PC обычно реализуется на уровне инфраструктуры (например, через XA-транзакции в Java или распределенные транзакции в базах данных). Он обеспечивает строгую согласованность, блокируя ресурсы на уровне СУБД до тех пор, пока координатор не примет решение. Это создает серьезные задержки в высоконагруженных системах и может привести к взаимным блокировкам (deadlocks).
TCC же работает на прикладном уровне, используя «логические блокировки». В TCC база данных завершает локальную транзакцию немедленно после фазы Try, освобождая системные ресурсы, но бизнес-логика продолжает считать ресурс зарезервированным. Это позволяет TCC масштабироваться гораздо лучше, чем 2PC.

TCC против паттерна Saga

Saga представляет собой последовательность локальных транзакций, где каждая транзакция фиксируется немедленно, а в случае ошибки выполняются компенсирующие действия. Основная проблема Saga заключается в отсутствии изоляции: промежуточные результаты работы саги видны другим пользователям, что может привести к феноменам «грязного чтения».
TCC решает эту проблему через предварительное резервирование в фазе Try. Ресурс не считается списанным или переданным другому пользователю до тех пор, пока не завершится фаза Confirm, что обеспечивает уровень изоляции, близкий к ACID транзакциям, при сохранении гибкости микросервисов.

Характеристика 2PC (XA) TCC Saga
Модель согласованности Строгая Квази-строгая Конечная (Eventual)
Уровень изоляции Полная (ACID) Высокая (через резервирование) Низкая (грязное чтение)
Производительность Низкая Высокая Очень высокая
Сложность разработки Низкая (инфраструктурная) Высокая (3 метода на сервис) Средняя (компенсация)
Блокировка ресурсов Уровень БД Уровень приложения Отсутствует

Технические вызовы и стратегии их решения

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

Обеспечение идемпотентности

Идемпотентность является фундаментальным требованием для методов Confirm и Cancel. В распределенной системе запрос может быть доставлен несколько раз из-за повторных попыток координатора или сетевых дублей. Если метод Confirm не является идемпотентным, повторный вызов может привести к двойному списанию средств или некорректному обновлению запасов.

Решение заключается в ведении локального журнала транзакций на стороне каждого участника. Перед выполнением операции сервис должен проверить, была ли данная транзакция уже подтверждена или отменена. Если да, сервис должен просто вернуть успешный ответ без выполнения каких-либо действий. Это требует наличия глобально уникального идентификатора транзакции (XID), который передается во всех вызовах.

Проблема «пустой отмены» (Null Rollback)

Ситуация «пустой отмены» возникает, когда участник получает запрос Cancel для транзакции, по которой он еще не получал или не смог успешно обработать запрос Try.
Это типичный сценарий при сетевых сбоях: запрос Try потерялся, координатор зафиксировал тайм-аут и инициировал откат всей глобальной транзакции. Если сервис-участник в ответ на такой Cancel выдаст ошибку, координатор будет бесконечно пытаться отменить транзакцию, которой «не существует».

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

Предотвращение «зависания» ресурсов (Anti-suspension)

Проблема «зависания» (suspension) является обратной стороной пустой отмены. Она происходит, когда задержанный запрос Try достигает участника после того, как тот уже выполнил Cancel для этой же транзакции. Если сервис просто выполнит этот запоздалый Try, он зарезервирует ресурсы, которые никогда не будут ни подтверждены, ни отменены повторно, так как координатор считает транзакцию уже закрытой.

Для решения этой проблемы используется механизм anti-suspension. Участник должен сохранять информацию об отмененных транзакциях (даже если Try для них не приходил). Если в будущем поступает запрос Try с идентификатором, который уже помечен как отмененный, сервис должен отклонить такой запрос.

Механизм Transaction Fence

Для автоматизации обработки идемпотентности, пустой отмены и anti-suspension такие платформы как Apache Seata внедряют механизм Transaction Fence. В основе этого механизма лежит использование специальной таблицы в базе данных участника — tcc_fence_log.

Поле таблицы Описание
xid Глобальный идентификатор транзакции.
branch_id Идентификатор ветви участника.
status Статус (TRIED, COMMITTED, ROLLBACKED, SUSPENDED).

Алгоритм работы Fence при обработке запросов следующий:

  1. На этапе Try: Система пытается вставить запись со статусом TRIED. Если запись уже существует со статусом SUSPENDED, это означает, что отмена уже произошла, и выполнение Try блокируется (anti-suspension).
  2. На этапе Confirm: Проверяется наличие записи. Если статус COMMITTED, действие игнорируется (идемпотентность). Если TRIED, выполняется бизнес-логика подтверждения, и статус обновляется на COMMITTED.
  3. На этапе Cancel: Если записи нет, вставляется запись со статусом SUSPENDED (обработка пустой отмены). Если статус TRIED, выполняется бизнес-логика отмены, и статус обновляется на ROLLBACKED.

Использование такого журнала транзакций в одной локальной транзакции БД с бизнес-изменениями гарантирует атомарность управления состоянием транзакции и самих данных.

Жизненный цикл ресурсов и предотвращение утечек

Управление ресурсами в TCC — это не только корректная реализация трех методов, но и долгосрочная стратегия предотвращения деградации системы из-за «забытых» резерваций.

Тайм-ауты и автоматическая очистка

В распределенной системе невозможно полагаться на мгновенные ответы. Если координатор не получает ответ от участника в фазе Try, он должен считать это отказом и инициировать отмену.
Однако если сбой происходит во второй фазе (Confirm/Cancel), ситуация становится критической. Протокол TCC гласит, что если Try завершился успешно, то вторая фаза обязана завершиться успехом. Следовательно, координатор должен повторять попытки Confirm или Cancel до победного конца.

Для предотвращения вечного удержания ресурсов в случае фатальных сбоев координатора или самих сервисов рекомендуется внедрять механизмы фоновой очистки. Специализированные планировщики задач должны сканировать записи о резервировании, которые находятся в состоянии «ожидания» дольше разумного срока (например, более 15 минут). Такие записи должны принудительно отменяться, чтобы вернуть ресурсы в общий пул.

Предотвращение утечек на уровне приложения

На уровне кода разработчики должны использовать паттерны, гарантирующие освобождение ресурсов.
Хотя в контексте распределенных транзакций это сложнее, чем использование блока try-with-resources в локальном коде, принципы схожи. Необходимо обеспечить, чтобы любая аллокация ресурса (например, запись в таблицу заморозки) имела четко определенный путь к деаллокации (Confirm или Cancel). Это включает мониторинг счетчиков открытых соединений, открытых файлов и, что наиболее важно в TCC, суммарного объема зарезервированных бизнес-ресурсов.

Проектирование бизнес-логики

Перевод бизнес-процесса на рельсы TCC требует глубокого переосмысления атомарных операций. Процесс можно разделить на пять основных этапов.

Шаг 1: Идентификация ресурсов и разделение фаз

Первым делом необходимо определить, какие ресурсы участвуют в транзакции и как их можно «заморозить». Операция, которая раньше была одним шагом (например, «снять деньги»), должна быть разделена. Половина операции уходит в Try (проверка и заморозка), вторая половина — в Confirm (окончательное списание).

Пример Фаза Try Фаза Confirm Фаза Cancel
Банковский перевод Проверка баланса, перевод суммы в поле frozen_funds. Списание из frozen_funds и уменьшение общего баланса. Удаление суммы из frozen_funds.
Складской учет Резервирование товара в таблице stock_reservation. Уменьшение физического остатка, удаление резерва. Удаление записи из stock_reservation.
Билетная система Пометка места как PENDING или RESERVED. Пометка места как SOLD. Пометка места как AVAILABLE.

Шаг 2: Проектирование интерфейсов участников

Каждый микросервис-участник должен реализовать три метода. Важно, чтобы интерфейс метода Try возвращал достаточно информации для последующей идентификации ресурса в фазах Confirm и Cancel. В REST-архитектуре это обычно URI созданного ресурса резервирования. Входящие параметры метода Confirm не должны требовать повторной передачи всей бизнес-информации; достаточно идентификатора транзакции и ссылки на резерв.

Шаг 3: Реализация идемпотентности и защиты

На этом этапе внедряется механизм Transaction Fence или его аналог. Разработчик должен обеспечить атомарность записи в локальный журнал транзакций и выполнения бизнес-действия. Это критически важно для предотвращения рассинхронизации, когда статус транзакции обновился, а данные — нет.

Шаг 4: Настройка координатора

Инициатор транзакции должен быть настроен на взаимодействие с Transaction Manager (TM). Это включает регистрацию в TM, запуск глобальной транзакции и корректную передачу контекста (XID) всем участникам через HTTP-заголовки или контекст вызова RPC. Координатор должен иметь настроенные политики повторных попыток для второй фазы.

Шаг 5: Мониторинг и обработка исключений

После запуска системы необходимо обеспечить видимость состояния распределенных транзакций. Это включает сбор метрик о количестве успешных фиксаций, откатов и, самое главное, о случаях частичного подтверждения, когда некоторые участники зафиксировали изменения, а другие — нет (что требует ручного вмешательства).

Изоляция и конкурентный доступ

Одной из самых сложных тем в TCC является обеспечение изоляции, то есть предотвращение влияния одной транзакции на другую до момента завершения. В отличие от 2PC, где изоляция обеспечивается на уровне БД (например, уровень Serializable), в TCC изоляция является «семантической».

Резервирование ресурсов в фазе Try фактически выполняет роль блокировки на прикладном уровне.
Если транзакция А зарезервировала 10 единиц товара, транзакция Б увидит уменьшенное количество доступного товара, хотя транзакция А еще не завершена. Это предотвращает «овербукинг» и другие проблемы конкуренции без удержания физических блокировок в базе данных.
Однако разработчик должен учитывать, что промежуточные состояния (зарезервированные ресурсы) могут быть видны в отчетах или через API, и это должно быть корректно обработано в бизнес-логике (например, отображение «доступного» и «замороженного» баланса отдельно).

Феномен грязного чтения в контексте TCC

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

Асинхронность и производительность

TCC демонстрирует высокую производительность благодаря асинхронной природе второй фазы.
После того как инициатор получил успешные ответы Try от всех участников и передал решение координатору, он может завершить свою работу, не дожидаясь окончания фазы Confirm у всех участников.
Координатор выполнит подтверждение в фоновом режиме. Это значительно снижает время отклика для конечного пользователя.

Однако за эту производительность приходится платить сложностью: система должна быть готова к тому, что в течение короткого времени данные будут находиться в состоянии «ожидания подтверждения». В высоконагруженных системах это состояние является нормой, и механизмы мониторинга должны уметь отличать нормальную задержку подтверждения от реального сбоя, требующего внимания.

Применение TCC в гетерогенных средах

Паттерн TCC не привязан к конкретному типу хранилища данных. Это делает его идеальным выбором для систем, объединяющих реляционные БД (SQL), NoSQL-хранилища, кэши (Redis) и очереди сообщений.
Поскольку логика резервирования и отмены пишется вручную, разработчик может реализовать «заморозку» ресурса даже в системе, которая изначально не поддерживает транзакции (например, отправка HTTP-запроса к внешнему API партнера с предварительным бронированием).

Тип системы Реализация Try Реализация Confirm Реализация Cancel
SQL БД Запись в таблицу резервов. Обновление основной таблицы, удаление резерва. Удаление записи из таблицы резервов.
NoSQL (Redis) Установка временного ключа-блокировки (SET NX EX). Удаление ключа-блокировки, обновление основного значения. Простое удаление ключа-блокировки.
Внешнее API Вызов метода бронирования (Hold). Вызов метода фиксации (Capture). Вызов метода отмены (Void/Release).

Заключение

Паттерн Try-Confirm-Cancel является мощным, но дорогим в реализации инструментом. Его следует выбирать в следующих случаях:

  1. Высокие требования к согласованности и изоляции: Когда недопустимы «грязные чтения», характерные для Saga, но необходимо избежать блокировок 2PC.
  2. Краткосрочные транзакции: TCC лучше всего работает, когда время между Try и Confirm составляет секунды или доли секунд. Для транзакций, длящихся часы или дни, более уместен паттерн Saga.
  3. Гетерогенные ресурсы: Когда транзакция охватывает системы, не поддерживающие XA или другие стандарты распределенных транзакций.
  4. Критическая бизнес-логика: Финансовые переводы, управление инвентарем, системы бронирования, где цена ошибки при частичном выполнении транзакции крайне высока.

Внедрение TCC требует от команды разработчиков дисциплины и глубокого понимания распределенных процессов. Однако правильно реализованная модель TCC обеспечивает ту степень надежности и прозрачности, которая необходима для построения современных отказоустойчивых корпоративных систем. Основной упор при реализации должен быть сделан на надежность второй фазы, идемпотентность операций и предотвращение утечек ресурсов через механизмы автоматической очистки и мониторинга.


Комментарии в Telegram-группе!