💳 Раздел 11 · Вопрос #1

Расшифруйте каждую букву ACID

Чтобы иметь возможность откатить транзакцию при ошибке:


🟢 Junior Level

ACID — это фундаментальный акроним, описывающий четыре ключевых требования к транзакционной системе (СУБД), гарантирующих надежность, целостность и предсказуемость обработки данных:

  • A — Atomicity (Атомарность): «Всё или ничего». Транзакция не может быть выполнена частично. Если одна из операций внутри транзакции завершается сбоем, все предыдущие изменения откатываются (ROLLBACK), и база данных возвращается в исходное состояние.
  • C — Consistency (Согласованность / Целостность): Транзакция переводит базу данных из одного корректного (валидного) состояния в другое корректное состояние. Все правила схемы (ограничения PRIMARY KEY, FOREIGN KEY, CHECK, NOT NULL, UNIQUE) и бизнес-инварианты должны строго соблюдаться.
  • I — Isolation (Изолированность): Параллельно выполняющиеся транзакции не должны оказывать взаимного влияния на результаты друг друга. Для каждой транзакции создается иллюзия, что она выполняется в системе единолично.
  • D — Durability (Долговечность / Стойкость): Если транзакция была успешно зафиксирована (COMMIT), внесенные ею изменения гарантированно сохраняются в энергонезависимой памяти и не будут потеряны даже при внезапном отключении электропитания или падении сервера СУБД.

Практический пример: банковский перевод

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class BankTransferService {

    private final AccountRepository accountRepository;

    public BankTransferService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    // Атомарная транзакция: либо оба счета обновятся, либо ни один
    @Transactional
    public void transferMoney(Long fromAccountId, Long toAccountId, Double amount) {
        // 1. Списание со счета А
        accountRepository.withdraw(fromAccountId, amount);

        // Имитация внезапной ошибки (баланс ушел в минус или сбой сети)
        if (amount > 100_000) {
            throw new IllegalStateException("Превышен лимит перевода");
        }

        // 2. Зачисление на счет Б
        accountRepository.deposit(toAccountId, amount);
    }
}

Аналогия: ACID напоминает отправку ценной посылки курьерской службой: посылка либо доставляется адресату целиком, либо возвращается отправителю без потерь (Atomicity); коробка обязана соответствовать габаритам и правилам почты (Consistency); курьеры не путают посылки разных клиентов на сортировочном складе (Isolation); после вручения под роспись запись в реестре доставки невозможно стереть или оспорить (Durability).


🟡 Middle Level

Архитектурная реализация каждого компонента ACID

+-------------------+---------------------------------------------------------------+
| Свойство ACID     | Механизм реализации в современных СУБД (PostgreSQL / MySQL)   |
+-------------------+---------------------------------------------------------------+
| Atomicity         | Undo Logs (MySQL) / Журнал упреждающей записи WAL (PostgreSQL)|
| Consistency       | Схемные Constraints + Application Business Logic Checks       |
| Isolation         | MVCC (многоверсионность) + Блокировки (Pessimistic / 2PL)     |
| Durability        | Write-Ahead Logging (WAL / Redo Log) + системный вызов fsync  |
+-------------------+---------------------------------------------------------------+

1. Atomicity: Журналирование изменений

Чтобы иметь возможность откатить транзакцию при ошибке:

  • MySQL (InnoDB): Создает сегмент Undo Log, куда перед изменением строки записывается ее предыдущее состояние. При команде ROLLBACK СУБД читает Undo Log в обратном порядке и возвращает исходные значения.
  • PostgreSQL: Не перезаписывает данные «на месте» (In-Place Update). Новая строка записывается как новая версия кортежа (Tuple), а старая помечается как удаленная транзакцией xmax. При ROLLBACK транзакция просто помечается в карте статусов (pg_xact) как ABORTED, и ее изменения игнорируются всеми транзакциями.

2. Consistency: Двухуровневая модель

Целостность данных гарантируется совместными усилиями базы данных и приложения:

  • Database-level: Декларативные ограничения схемы:
    ALTER TABLE accounts ADD CONSTRAINT chk_positive_balance CHECK (balance >= 0);
    

    Если операция нарушает ограничение CHECK, СУБД аварийно прерывает операцию и инициирует откат всей транзакции.

  • Application-level: Закон сохранения баланса (сумма дебета должна равняться сумме кредита). База данных не может знать специфику предметной области — эту ответственность несет программный код.

3. Isolation: MVCC против блокировок

Традиционные блокировки (Pessimistic 2PL) блокировали чтение строк во время их обновления. Современные СУБД используют MVCC (Multi-Version Concurrency Control):

  • Пишущие транзакции создают новые версии строк, не блокируя читателей.
  • Читающие транзакции видят снимок данных (Snapshot) на момент старта транзакции или оператора.
  • Читатели не блокируют писателей, а писатели не блокируют читателей.

4. Durability: Упреждающая запись в журнал (WAL)

Прямая запись измененных страниц данных (Data Pages) на диск в случайные сектора слишком медленная. Поэтому применяется концепция Write-Ahead Logging (WAL):

  1. При изменении данных новая информация сначала последовательно записывается в буфер журнала в оперативной памяти.
  2. При вызове COMMIT буфер журнала принудительно сбрасывается на физический диск с помощью вызова fsync().
  3. Сами страницы данных в оперативной памяти (Buffer Pool / shared_buffers) остаются «грязными» (Dirty Pages) и сбрасываются на диск асинхронно в фоновом режиме (Checkpointer).
  4. Если питание сервера отключится, при перезапуске процедура Crash Recovery перечитает WAL с диска и повторно накатит подтвержденные изменения (REDO).

🔴 Senior Level

Глубокий анализ реализации в PostgreSQL и MySQL (InnoDB)

Atomicity & Crash Recovery: Алгоритм ARIES

Процесс восстановления после сбоев в реляционных СУБД основан на классическом алгоритме ARIES (Algorithms for Recovery and Isolation Exploiting Semantics), состоящем из трех фаз:

  1. Analysis: Анализ WAL с момента последнего чекпоинта (Checkpoint) для определения активных транзакций на момент сбоя и страниц, находившихся в буферном пуле.
  2. Redo: Повтор абсолютно всех изменений (включая не подтвержденные транзакции) вперед по журналу до точки краша, восстанавливая точное состояние страниц в памяти.
  3. Undo: Откат назад всех транзакций, которые были активны в момент сбоя и не успели выполнить COMMIT.

Архитектурное различие MVCC: PostgreSQL Heap Bloat vs MySQL Undo Log

  • PostgreSQL (Append-Only MVCC): Каждая строка таблицы хранит служебные поля xmin (ID транзакции, создавшей строку) и xmax (ID транзакции, удалившей/обновившей строку). При UPDATE в файл таблицы (Heap) дописывается новая версия строки. Старые «мертвые» версии строк остаются на диске до тех пор, пока процесс VACUUM не очистит их. При интенсивных апдейтах это приводит к распуханию таблиц (Table Bloat) и деградации производительности.
  • MySQL InnoDB (Rollback Segment): Новая версия строки записывается непосредственно поверх старой прямо в страницу таблицы. Старая версия вытесняется в системное табличное пространство Undo Tablespace в виде обратной дельты (Undo Log Chain). Если читателю нужна старая версия, он на лету собирает ее из цепочки Undo-логов. Таблицы не распухают, но длинные транзакции переполняют Undo Log.

Компромиссы Durability в High-Load архитектуре

Физический сброс данных на диск через fsync() при каждом коммите ограничивает пропускную способность (TPS) характеристиками IOPS накопителя.

-- PostgreSQL: Асинхронный коммит
SET synchronous_commit = off;
  • Плюсы: Транзакция возвращает успех клиенту сразу после записи в буфер WAL, не дожидаясь fsync(). Пропускная способность на запись возрастает в 3–10 раз.
  • Минусы (Trade-off): Нарушается строгое свойство Durability. При внезапном сбое ОС или питания могут быть безвозвратно потеряны последние 200–600 мс подтвержденных транзакций (пока демон walwriter не сбросил буфер).

4 Tricky Questions

1. В чем фундаментальная разница между Consistency в аббревиатуре ACID и Consistency в CAP-теореме?

Ответ: Это две совершенно разные концепции, совпадающие только по названию:

  • Consistency в ACID (C-ACID) — Согласованность / Валидность: Означает соблюдение инвариантов и ограничений схемы данных (FOREIGN KEY, UNIQUE, CHECK). База данных не допускает появления невалидных данных внутри одной системы.
  • Consistency в CAP-теореме (C-CAP) — Линеаризуемость / Атомарная видимость: Характеристика распределенной системы. Означает, что все реплики (узлы кластера) в один и тот же момент времени видят абсолютно одинаковые данные. Любое чтение возвращает результат последней завершенной записи независимо от того, к какому узлу обратился клиент.

2. Если транзакция закоммичена (COMMIT), означает ли свойство Durability, что данные физически записаны в файлы таблиц на диске?

Ответ: Нет, не означает. В подавляющем большинстве СУБД (PostgreSQL, MySQL, Oracle) данные физически сбрасываются только в журнал предзаписи (WAL / Redo Log). Сами страницы табличных пространств (.ibd в MySQL или base/* в PostgreSQL) продолжают оставаться в оперативной памяти в виде грязных страниц (Dirty Pages) в течение минут. Они будут сброшены на диск асинхронно фоновым процессом сброса контрольных точек (Checkpointer). Durability гарантируется тем, что в случае сбоя сервер при перезапуске воспроизведет незаписанные страницы из сохраненного журнала WAL.


3. Гарантирует ли аннотация Spring @Transactional соблюдение всех четырех свойств ACID по умолчанию?

Ответ: Нет, свойство Isolation по умолчанию гарантируется лишь частично. По умолчанию в Spring аннотация @Transactional использует уровень изоляции базовой базы данных (Isolation.DEFAULT):

  • Для PostgreSQL и Oracle это Read Committed.
  • Для MySQL (InnoDB) это Repeatable Read.

На уровне Read Committed параллельные транзакции подвержены аномалиям: Non-Repeatable Read (неповторяющееся чтение) и Phantom Read (фантомное чтение). Таким образом, абсолютная изоляция транзакций отсутствует, если разработчик явно не настроит уровень Isolation.SERIALIZABLE или не применит пессимистические блокировки (SELECT FOR UPDATE).


4. Что такое Deferred Constraints (отложенные ограничения) в реляционных базах данных и как они влияют на свойство Consistency?

Ответ: В реляционных СУБД (например, PostgreSQL, Oracle) ограничения целостности могут быть объявлены с модификатором DEFERRABLE INITIALLY DEFERRED. В штатном режиме ограничения (FOREIGN KEY, CHECK) проверяются немедленно после каждого SQL-вызова (INSERT, UPDATE). Если ограничение объявлено как DEFERRED, СУБД разрешает временное нарушение целостности данных внутри транзакции (например, создание дочерней записи до создания родительской при циклической ссылке внешних ключей). Проверка целостности откладывается до финального вызова COMMIT. Если к моменту коммита инвариант не восстановился, транзакция откатывается. Это обеспечивает гибкость без ущерба финальному свойству Consistency.


🎯 Шпаргалка для интервью

  • Расшифровка ACID:
    • A (Atomicity): Всё или ничего (реализуется через Undo Log / WAL).
    • C (Consistency): Сохранение инвариантов схемы (FK, UNIQUE, CHECK) и бизнес-правил.
    • I (Isolation): Независимость параллельных транзакций (реализуется через MVCC и блокировки).
    • D (Durability): Гарантия сохранности закоммиченных данных (WAL + fsync на диск).
  • MVCC принцип: «Читатели не блокируют писателей, а писатели не блокируют читателей».
  • WAL принцип: Журнал изменений последовательно сбрасывается на диск через fsync() при коммите; страницы данных в памяти (dirty pages) сбрасываются позже асинхронно через Checkpoint.
  • Ловушки:
    • Не путать C в ACID (целостность инвариантов) с C в CAP (линеаризуемость реплик).
    • Коммит гарантирует запись в WAL, а не в файлы таблиц.
    • @Transactional по умолчанию работает на уровне изоляции БД (Read Committed в Postgres), допуская аномалии чтения.

Связанные темы