Практическая ситуация: перевод изменяет данные в двух SQL базах, но одна из них становится недоступна перед...

Практическая ситуация: перевод изменяет данные в двух SQL-базах, но одна из них становится недоступна перед фиксацией. Как двухфазный коммит предотвращает частичную фиксацию?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Исторический контекст

Обычная SQL-транзакция обеспечивает атомарность внутри одного экземпляра СУБД. Когда одна логическая операция затрагивает несколько независимых баз или других транзакционных ресурсов, локальный COMMIT каждой системы уже не даёт общей гарантии: один участник может успешно завершиться, а другой — стать недоступным.

2PC появился как протокол координации таких распределённых транзакций. Его исходная задача — согласовать единое решение «зафиксировать» или «откатить» между несколькими участниками.

Постановка проблемы

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

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

Подробное решение

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

Во второй фазе координатор принимает общее решение. Если все участники ответили готов, он сохраняет решение commit и рассылает его всем. Если хотя бы один ответил не готов или не ответил до принятия решения, координатор выбирает rollback и сообщает об откате.

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

Координатор и участники используют журналы для восстановления после сбоя. Если координатор уже записал глобальное решение о фиксации, участник при восстановлении должен выполнить COMMIT; если решение не было принято, конкретное поведение зависит от реализации и протокола восстановления, но участник не должен произвольно создавать частичную фиксацию.

Главный компромисс — атомарность ценой задержки, блокировок и сложности. 2PC требует сетевых обменов, хранения состояния prepared, повышает время удержания блокировок и может стать узким местом. Он также не делает внешние действия, например отправку письма или вызов платёжного API, атомарными с SQL-транзакцией.

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

Ситуация из практики

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

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

Что кандидаты часто упускают

1. Вопрос: Почему участник 2PC не должен освобождать блокировки сразу после ответа готов?

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

2. Вопрос: Почему 2PC не устраняет все виды распределённых сбоев?

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

3. Вопрос: Чем опасно решение о COMMIT, принятое координатором, но не доставленное одному участнику?

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