Что означает «согласованность» в ACID и почему успешная фиксация транзакции не гарантирует соблюдение любог...

Что означает «согласованность» в ACID и почему успешная фиксация транзакции не гарантирует соблюдение любого бизнес-инварианта?

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

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

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

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

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

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

Модель ACID описывает свойства, необходимые для надёжного выполнения транзакций в условиях ошибок и конкуренции. При этом «согласованность» относится к переходам между состояниями, допустимыми по известным правилам, а не к пониманию СУБД всех бизнес-смыслов приложения.

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

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

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

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

СУБД может автоматически контролировать только те условия, которые ей известны. К ним относятся, например, PRIMARY KEY, UNIQUE, FOREIGN KEY, CHECK и ограничения NOT NULL. Если проверка нарушена, оператор или транзакция отклоняется.

CREATE TABLE subscriptions ( id integer PRIMARY KEY, customer_id integer NOT NULL, active boolean NOT NULL, CONSTRAINT one_active_subscription UNIQUE (customer_id, active) );

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

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

SERIALIZABLE может обеспечить эквивалентность некоторому последовательному порядку выполнения, но не исправляет ошибочную логику и не включает в транзакцию операции, выполненные вне неё. Кроме того, СУБД может отклонить транзакцию из-за конфликта, поэтому приложение должно уметь повторять безопасные операции.

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

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

Сервис подписок должен гарантировать одну активную подписку на клиента. Первый вариант — проверять это условие в приложении перед вставкой. Он прост, но при параллельных запросах допускает гонку: обе проверки могут завершиться до вставки второй записи.

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

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

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

1. Достаточно ли ограничения CHECK для правила, зависящего от нескольких строк?

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

2. Гарантирует ли уровень SERIALIZABLE любой бизнес-инвариант?

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

3. Может ли валидация в приложении заменить ограничение базы данных?

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