«У нас всё записывается в транзакции» — полезный ответ на вопрос о надёжности, но слишком короткий. Хочется уточнить: что именно входит в это «всё»? Только изменения в базе, обращение к платёжному провайдеру или ещё и письмо, которое клиент уже получил? От ответа зависит, какую ошибку система сможет исправить сама, а какую придётся разбирать людям.
ACID описывает свойства транзакций: атомарность, согласованность, изоляцию и долговечность. Для руководителя здесь важнее всего граница обещания. Какие изменения компания считает одной неделимой операцией и действительно ли технология обеспечивает это в выбранных границах?
Две записи, которые нельзя разлучать
Допустим, внутри одной учётной системы перераспределяют бюджет между проектами. У первого проекта лимит уменьшают, у второго увеличивают, затем сохраняют основание перевода. Все данные находятся в одной транзакционной базе. Если приложение остановится после первого изменения, компании не нужен «почти выполненный перевод» с пропавшей частью бюджета.
Эти изменения объединяют в транзакцию. При успешном завершении база фиксирует их вместе — выполняет commit. Если операция отменяется до фиксации, её изменения откатываются. Другие операции не должны увидеть сохранённую половину такого перевода. Это атомарность: результат целиком или отсутствие результата, а не обещание, что любое действие с деньгами в любой системе автоматически можно отменить.
Внешний банковский перевод в этот пример не входит. Если система отправила отдельную команду банку, локальный rollback не заставит банк забыть её. Смешивать внутреннюю запись и внешний эффект особенно опасно в обсуждении, где оба действия называют одним словом «платёж».
База не знает правил, которые ей не сообщили
Согласованность означает сохранение установленных правил данных при корректно построенной операции. Но база не догадается, что нельзя перевести больше доступного лимита или согласовать собственную заявку. Часть требований выражается ограничениями базы: уникальностью номера операции, обязательностью полей, связями между записями. Остальные проверяет приложение.
Если разработчик написал неправильный расчёт, транзакция может безупречно сохранить неправильный результат. Поэтому вопрос «поддерживает ли база ACID?» не заменяет разговор о конкретном правиле. Для перевода выше надо сохранить общий бюджет, не допустить запрещённого отрицательного остатка и не выполнить повторное поручение как новое. У каждого требования должен быть свой механизм защиты.
Изоляция касается одновременной работы. Две операции могут по отдельности проверить один и тот же свободный лимит и обе решить, что средств достаточно. Атомарность каждой ещё не исключает неправильного общего результата. Нужно выбрать уровень изоляции, условное обновление или блокировку, которые защищают именно это правило.
Более строгая изоляция не обязательно означает, что все запросы выстроятся в одну очередь. Некоторые реализации обнаруживают опасное сочетание операций и отменяют одну из них. Приложение должно заново выполнить всю отклонённую транзакцию, включая проверки, либо сообщить о невозможности завершить действие.
Когда «сохранено» действительно означает сохранено
Долговечность — обещание сохранить зафиксированный результат при предусмотренных отказах. Важно, какое подтверждение база отправляет приложению. Например, в PostgreSQL асинхронный commit позволяет сообщить об успехе до надёжной записи соответствующего журнала на диск; авария в этот промежуток может привести к потере последних подтверждённых изменений. Такая настройка меняет гарантию, а не просто ускоряет тот же самый сервис.
При этом долговечность не равна защите от любого бедствия. Удаление данных с достаточными правами — тоже изменение, которое база способна надёжно сохранить. Полная потеря площадки требует отдельного плана. Репликация, резервные копии и проверенное восстановление решают другие части задачи. Одного слова ACID для обещания «ничего никогда не потеряем» недостаточно.
Операция завершилась, но ответ не пришёл
Есть ещё неприятный случай: commit состоялся, а соединение оборвалось до получения ответа приложением. База знает результат, клиент — нет. Ошибка связи сама по себе не доказывает, что перевод отменён.
Повторить поручение с новым номером в такой ситуации рискованно. Нужен устойчивый идентификатор операции, возможность узнать её состояние и защита от повторного исполнения. Транзакция гарантирует целостность одного исполнения; она не понимает автоматически, что два запроса выражают одно намерение пользователя.
Если после перевода требуется уведомить другую систему, намерение отправить сообщение можно сохранить вместе с изменениями бюджета. Затем отдельный процесс доставит его получателю. Это Transactional Outbox. Он закрывает разрыв между сохранением факта и будущей публикацией, но получателю всё равно придётся учитывать повторную доставку.
Граница должна оставаться управляемой
Держать транзакцию открытой, пока сотрудник читает документ или внешний сервис отвечает на запрос, обычно плохая идея. Всё это время могут удерживаться соединение, блокировки и другие ресурсы. Предварительные расчёты и человеческое согласование можно выполнить раньше, а перед записью коротко проверить актуальные условия. Старое одобрение нельзя без проверки применять к уже изменённым данным.
Распределённые транзакции существуют, но требуют участия систем в согласованном протоколе и отдельной работы с отказами. Они не возникают от того, что несколько HTTP-вызовов поместили между BEGIN и COMMIT. Если независимые участники уже выполнили действия, восстановление процесса может потребовать компенсаций. Возврат или корректировка не обеспечивают ту же неделимость, что локальная транзакция.
На приёмке полезно остановить процесс перед commit, затем отдельно потерять ответ после commit и, наконец, одновременно запустить два поручения, конкурирующих за один лимит. Для этих случаев нужны разные доказательства корректности. Руководителю достаточно увидеть, что в первом не осталось половины операции, во втором её не повторили, а в третьем сохранили ограничение бюджета. Именно такое поведение имеет смысл обещать бизнесу.