Транзакции, ACID и конкурентный доступ

Транзакции и ACID спрашивают даже у аналитиков — потому что от ответа зависит, попадут ли в требования сценарии «а если упало посередине».

Транзакция — набор операций, который выполняется целиком или не выполняется вовсе. ACID — четыре свойства, которые СУБД обещает транзакции: атомарность, согласованность, изолированность, долговечность.

Вопрос 1. Что такое ACID? Объясните на примере

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

  • Atomicity (атомарность). Списание с одного счёта и зачисление на другой происходят вместе. Если сбой между операциями — откатывается всё, деньги не исчезают.
  • Consistency (согласованность). После транзакции соблюдены все правила целостности: внешние ключи, ограничения, проверки. Баланс не станет отрицательным, если это запрещено ограничением.
  • Isolation (изолированность). Параллельные транзакции не видят промежуточных состояний друг друга. Насколько строго — определяет уровень изоляции.
  • Durability (долговечность). После подтверждения (commit) данные переживут отключение питания — они уже записаны в журнал на диск.
CREATE TABLE account (
  id      INTEGER PRIMARY KEY AUTOINCREMENT,
  owner   TEXT NOT NULL,
  balance REAL NOT NULL CHECK (balance >= 0)
);

INSERT INTO account (owner, balance) VALUES ('Анна', 1000.0), ('Борис', 500.0);

-- перевод 300 рублей: обе операции в одной транзакции
BEGIN TRANSACTION;
UPDATE account SET balance = balance - 300 WHERE owner = 'Анна';
UPDATE account SET balance = balance + 300 WHERE owner = 'Борис';
COMMIT;

SELECT owner, balance FROM account ORDER BY owner;

Запустите блок: получится «Анна 700, Борис 800». Если бы между двумя UPDATE произошёл сбой, ROLLBACK вернул бы исходные 1000 и 500 — деньги не потерялись бы. Ограничение CHECK (balance >= 0) — иллюстрация согласованности: перевод 2000 рублей просто не пройдёт.

Вопрос 2. Какие бывают уровни изоляции и какие аномалии они закрывают?

Что проверяют. Средний и старший уровень. Достаточно знать четыре уровня и три аномалии и уметь связать их таблицей.

УровеньГрязное чтениеНеповторяющееся чтениеФантомы
Read Uncommittedвозможновозможновозможны
Read Committedнетвозможновозможны
Repeatable Readнетнетвозможны*
Serializableнетнетнет

Что означают аномалии по-человечески:

  • Грязное чтение — вы прочитали чужие неподтверждённые изменения, а их потом откатили.
  • Неповторяющееся чтение — прочитали строку дважды внутри одной транзакции и получили разные значения, потому что кто-то её изменил и закоммитил.
  • Фантомы — повторили один и тот же запрос по условию и получили новые строки, которых не было. Звёздочка в таблице: в PostgreSQL на уровне Repeatable Read фантомов не будет благодаря снимку данных, в стандарте SQL — допускаются.

Аналитику это нужно, чтобы формулировать требования к конкурентному доступу. Классический пример: два менеджера одновременно резервируют последнюю единицу товара. Требование должно явно описывать поведение: «резерв выполняется в транзакции с блокировкой строки остатка; второй запрос получает 409 и сообщение "товар закончился"».

Вопрос 3. Что такое оптимистичная и пессимистичная блокировка?

Что проверяют. Практический навык: как описать одновременное редактирование одной карточки двумя пользователями.

Пессимистичная блокировка: пользователь захватывает запись на редактирование, остальные видят «карточка занята Ивановым». Подходит для длинных редактирований и небольшого числа пользователей. Минус — забытые блокировки, нужен таймаут.

Оптимистичная блокировка: никто ничего не захватывает, но у записи есть версия. При сохранении версия сверяется; если она изменилась — сохранение отклоняется с предложением обновить данные. Дешевле и лучше масштабируется, поэтому в веб-системах используется чаще.

Оптимистичная блокировка в контракте API

GET  /orders/1024        → { "id":1024, "status":"new", "version": 7 }

PATCH /orders/1024
If-Match: "7"
{ "status": "paid" }

200 OK        → сохранено, version стал 8
409 Conflict  → { "code":"VERSION_CONFLICT",
                  "detail":"Заказ изменён другим пользователем",
                  "currentVersion": 8 }

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

Ключи и связи: короткий блок, который любят спросить следом

  • Первичный ключ (PK) — уникально идентифицирует строку, не может быть NULL.
  • Естественный ключ — значение из предметной области (ИНН, номер паспорта). Плюс — осмысленность, минус — меняется и повторяется чаще, чем кажется.
  • Суррогатный ключ — искусственный идентификатор (автоинкремент, UUID). Не несёт смысла и потому не меняется. Практика по умолчанию, а естественный ключ выносится в уникальный индекс.
  • Внешний ключ (FK) — ссылка на первичный ключ другой таблицы, гарантирует ссылочную целостность.
  • Каскадное удаление — вопрос к бизнесу, а не к разработчику: удаление клиента не должно молча уносить его заказы. Обычно вместо удаления применяют мягкое (пометкой) удаление.

Типичные ошибки кандидатов

  • Расшифровывают ACID, но не могут привести пример, где нарушение атомарности видно бизнесу.
  • Считают, что уровень изоляции по умолчанию — Serializable. В большинстве СУБД это Read Committed.
  • Не описывают конкурентный доступ в требованиях, и «последний сохранивший затирает всех» обнаруживается на проде.
  • Пытаются растянуть транзакцию на несколько сервисов. Между сервисами транзакции нет — там сага с компенсирующими операциями.
  • Ставят каскадное удаление, не спросив бизнес, допустимо ли терять историю.

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

Транзакция выполняется целиком или откатывается. ACID: атомарность — списание и зачисление вместе; согласованность — не нарушаются ограничения целостности; изолированность — параллельные транзакции не видят промежуточных состояний; долговечность — после commit данные переживут сбой. Уровни изоляции от Read Uncommitted до Serializable закрывают грязное чтение, неповторяющееся чтение и фантомы, по умолчанию обычно Read Committed. Как аналитик я обязан описать конкурентный доступ: для одновременного редактирования — оптимистичная блокировка по версии записи с ответом 409 и понятным сообщением, а не молчаливая перезапись. И помню, что между сервисами транзакции нет: там сага с компенсирующими операциями.

Проверьте себя
1. Что означает буква I в ACID?
AЦелостность данных после подтверждения транзакции
BИзолированность: параллельные транзакции не видят промежуточных состояний друг друга
CНеделимость: транзакция выполняется целиком или откатывается
DИндексируемость всех изменяемых таблиц
2. Два пользователя одновременно редактируют одну карточку. Что описывает оптимистичная блокировка?
AЗапись захватывается первым пользователем, остальные видят «занято»
BИзменения объединяются автоматически без участия пользователя
CСохранение проверяет версию записи и при расхождении отклоняется с 409
DПобеждает тот, кто сохранил последним
3. Почему в большинстве систем первичным ключом делают суррогатный идентификатор, а не ИНН или номер паспорта?
AЕстественные ключи занимают больше места
BЕстественные ключи меняются и повторяются чаще, чем кажется, а суррогатный не несёт смысла и потому стабилен
CСУБД не умеют строить индексы по текстовым полям
DТак требует третья нормальная форма