При разборе требования две записи должны измениться «одновременно». Как определить, должны ли они находиться в одной транзакционной границе?
Записи должны входить в одну транзакцию, если между ними существует бизнес-инвариант, который нельзя нарушить даже на короткое время, а хранилище может атомарно зафиксировать обе операции. Если записи принадлежат разным сервисам или ресурсам, общей локальной транзакции обычно нет: нужно либо изменить границу владения данными, либо явно принять промежуточное состояние и использовать согласованный процесс, например сагу.
Ключевой критерий — не физическая близость таблиц, а требование к атомарности бизнес-операции и допустимость частичного результата.
Транзакции появились как способ скрыть от приложения частичные результаты операций при сбоях, конкуренции и восстановлении после отказа. Свойства атомарности, согласованности, изоляции и долговечности позволяют рассматривать группу изменений как одну логическую операцию.
По мере разделения систем данные стали распределяться между сервисами и хранилищами. Локальная транзакция перестала автоматически охватывать весь бизнес-процесс, поэтому границу атомарности пришлось отделять от границы пользовательского сценария: одна операция пользователя может состоять из нескольких независимо фиксируемых шагов.
Фраза «изменить одновременно» неоднозначна. Она может означать строгую атомарность — наблюдатель никогда не должен увидеть только одно изменение, — или лишь требование к конечному результату, который должен наступить после успешного завершения процесса.
Если ошибочно объединить независимые данные в одну транзакцию, растут блокировки, задержки и связанность компонентов. Если, наоборот, разделить изменения, между ними может возникнуть запрещённое состояние: например, баланс уменьшился, а резерв не появился.
Особенно опасна попытка сделать вид, что несколько сервисов участвуют в одной обычной транзакции. Сетевой сбой после фиксации первого изменения может оставить систему в частично обработанном состоянии, которое нельзя исправить простым повтором.
Сначала сформулируйте инвариант: какое условие должно быть истинным после каждой видимой фиксации? Например, сумма доступного и зарезервированного остатка должна равняться общему остатку. Если нарушение этого условия недопустимо, связанные изменения должны выполняться в одной транзакционной границе или за этой границей должен находиться компонент, владеющий инвариантом.
Затем определите владельца данных. Если обе записи принадлежат одному сервису и находятся в одном поддерживающем транзакции хранилище, обычно разумно изменить их атомарно. При этом транзакция должна быть короткой: внешние сетевые вызовы, ожидание пользователя и длительные вычисления в неё не включают.
Если данные принадлежат разным сервисам, есть несколько вариантов:
Компенсация не равна откату. Откат атомарной транзакции возвращает хранилище к прежнему состоянию, а компенсация выполняет новую бизнес-операцию, которая должна нейтрализовать уже зафиксированный шаг. Поэтому компенсация может быть невозможна, неполной или требовать ручной обработки.
Также нужно определить наблюдаемость промежуточного состояния. Даже если конечная согласованность допустима, API не должен преждевременно сообщать об окончательном успехе. Клиенту можно вернуть состояние обработки, а чтение должно явно отражать, что процесс ещё не завершён.
В системе доставки при создании отправления нужно записать заказ и уменьшить доступный остаток упаковочных материалов. Команда сначала поместила обе операции в одну транзакцию базы заказов, но остатки фактически хранились в другом сервисе. В результате разработчики либо получили ложное ощущение атомарности, либо стали зависеть от распределённой транзакции.
Рассматривались три варианта. Синхронный вызов сервиса материалов после записи заказа был простым, но оставлял заказ без резерва при тайм-ауте. Распределённая транзакция давала строгую атомарность, но увеличивала связанность и сложность отказоустойчивости. Сага с событием создания заказа, идемпотентным резервированием и компенсацией отмены позволила сохранить независимое владение сервисами, но потребовала явных промежуточных статусов и фоновой обработки.
Выбрали сагу, потому что бизнес допускал короткий период ожидания резерва, а заказ нельзя было безусловно считать готовым до подтверждения материалов. Главный результат — частичный сбой стал штатным состоянием процесса с понятным восстановлением, а не скрытым нарушением данных.
1. Достаточно ли поместить две операции в одну транзакцию базы данных?
Нет. Транзакция защищает только тот набор ресурсов, который действительно участвует в её механизме фиксации. Если одно изменение выполнено в другой базе, через внешний сервис или после отправки сообщения, локальная транзакция не делает операции атомарными. Нужно либо изменить границу владения, либо проектировать межсервисное согласование отдельно.
2. Чем отличается требование к атомарности от требования к конечной согласованности?
Атомарность запрещает наблюдаемое частичное выполнение группы изменений. Конечная согласованность допускает промежуточное состояние, но требует, чтобы система при отсутствии новых сбоев пришла к корректному результату.
Эти требования влияют на API и модель данных. При конечной согласованности нужны статусы процесса, повторная обработка, идемпотентность и правила разрешения зависших операций. Нельзя просто назвать асинхронный процесс успешным, если бизнес-результат ещё не достигнут.
3. Когда следует не добавлять компенсацию, а менять границу данных?
Границу стоит менять, если нарушение инварианта недопустимо, компенсация необратима или часто требует ручного вмешательства. Например, финансовое списание и запись обязательства могут требовать единого владельца состояния либо специализированного механизма согласования.
Если же данные имеют разные жизненные циклы, принадлежат разным командам, а бизнес допускает промежуточные статусы, разделение обычно оправдано. Компромисс нужно оценивать по цене строгой атомарности, частоте отказов, обратимости действий и допустимой задержке согласования.