сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Базы данных в Python

Транзакции и уровни изоляции

Транзакции и уровни изоляции

Транзакции и изоляция - тема, где интуиция подводит: люди помнят ACID, но плывут на аномалиях и на том, что atomic не спасает от гонки на строке. Собес проверяет это, потому что деньги и остатки на складе теряются именно тут.

Типовые формулировки: «что такое ACID?», «какой уровень изоляции по умолчанию в PostgreSQL?», «как не потерять деньги при параллельном списании».

ACID и лестница аномалий

ACID (atomicity, consistency, isolation, durability): Atomicity (всё или ничего), Consistency (только валидные состояния), Isolation (параллельные транзакции не мешают друг другу), Durability (зафиксированное переживёт сбой). Уровни изоляции это про букву I, и они образуют лестницу защиты от аномалий.

Аномалии по нарастающей. Грязное чтение (dirty read) - видеть чужие НЕзафиксированные изменения; возможно на READ UNCOMMITTED, а в PostgreSQL минимум - READ COMMITTED, так что грязного чтения там нет вовсе. Non-repeatable read - между двумя SELECT чужой commit изменил строку. Phantom - повторный запрос по условию видит новые чужие строки.

// READ COMMITTED (дефолт PostgreSQL) убирает dirty read, но допускает non-repeatable read; REPEATABLE READ/снапшот защищает и от фантомов, SERIALIZABLE - строжайший.

ACID
атомарность, согласованность, изоляция, устойчивость
dirty/non-repeatable/phantom
аномалии изоляции разной силы

atomic не сериализует доступ к строке

Частая ловушка: обернул списание в транзакцию (atomic) и думаешь, что гонка ушла. Нет. atomic обеспечивает АТОМАРНОСТЬ набора операций - всё или ничего, но не сериализует доступ к конкретной строке: два воркера в двух транзакциях всё равно прочитают один баланс и оба спишут.

Гонку на строке снимает пессимистичная блокировка SELECT ... FOR UPDATE: она блокирует строку до конца транзакции, второй воркер ждёт. Либо оптимистично - версионным полем с проверкой при UPDATE. Понижать же изоляцию до READ UNCOMMITTED «для скорости» - не защита, а ослабление, оно только добавляет аномалий.

// Различай non-repeatable read (существующая строка изменилась) и phantom (появились новые строки по условию) это разные аномалии и разные уровни защиты.

-- два воркера списывают баланс безопасно:
SELECT balance FROM acc WHERE id=1 FOR UPDATE;  -- строка заперта
UPDATE acc SET balance = balance - 100 WHERE id=1;
READ COMMITTED
дефолт PostgreSQL: без грязного чтения
SELECT FOR UPDATE
пессимистичная блокировка строки от гонки

Как отвечать: «Два воркера списывают с баланса - как не потерять деньги?»

Обёртки в транзакцию тут мало: atomic гарантирует атомарность набора операций, но не сериализует доступ к строке - оба воркера прочитают один и тот же баланс и оба спишут, одно списание потеряется. Снимаю гонку пессимистичной блокировкой: SELECT ... FOR UPDATE запирает строку баланса до конца транзакции, и второй воркер ждёт, пока первый закоммитит, а потом видит уже уменьшенный баланс. Альтернатива - оптимистичная блокировка через версионное поле или сразу атомарный UPDATE вида balance = balance - 100 с проверкой, что хватает средств. Понижать изоляцию ради скорости тут бессмысленно это ослабление, а не защита.

Развеяно ключевое заблуждение (atomic ≠ сериализация), дано пессимистичное и оптимистичное решение и отвергнут ложный ход с изоляцией - точное понимание конкурентного доступа.

На чём валят

  • Считать, что atomic сам убирает гонку на строке - он про атомарность набора, не про сериализацию доступа.
  • Понижать изоляцию до READ UNCOMMITTED «для скорости» это ослабление, а не защита.
  • Путать non-repeatable read (меняется строка) и phantom (появляются новые строки).
  • Думать, что в PostgreSQL бывает грязное чтение - там минимум READ COMMITTED, dirty read исключён.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #transactions_isolation1 / 5
    Что такое «грязное чтение» (dirty read)?
    A)Чтение строки без наложения на неё блокировки на запись
    B)Чтение чужих ещё не зафиксированных изменений
    C)Чтение строки, у которой повреждён индекс на стороне СУБД
    D)Чтение из реплики, отставшей от мастера на несколько секунд
    показать ответ и разбор
    +B)Чтение чужих ещё не зафиксированных изменений

    // разбор: Грязное чтение — транзакция видит изменения другой, ещё не зафиксированной транзакции; если та откатится, ты прочитал то, чего «не было». Возможно только на уровне READ UNCOMMITTED. В PostgreSQL этого уровня фактически нет — минимальный там READ COMMITTED.

  2. #transactions_isolation2 / 5
    Уровень READ COMMITTED (дефолт PostgreSQL) — какую аномалию он ещё допускает?
    A)Чтение из будущего — значений, которых ещё нет в базе
    B)Полную потерю зафиксированных данных после перезапуска базы
    C)Грязное чтение незафиксированных чужих изменений строки
    D)Неповторяемое чтение: два SELECT дают разные значения
    показать ответ и разбор
    +D)Неповторяемое чтение: два SELECT дают разные значения

    // разбор: READ COMMITTED исключает грязное чтение (видны только зафиксированные данные), но допускает non-repeatable read: между двумя SELECT в одной транзакции другая успела закоммитить UPDATE — и значение изменилось. Чтобы читать стабильный снапшот, берут REPEATABLE READ.

  3. #transactions_isolation3 / 5
    Что такое фантомное чтение (phantom read)?
    A)Значение одной и той же строки меняется между двумя чтениями
    B)Транзакция читает собственные незакоммиченные изменения строк
    C)Строка исчезает из результата из-за сбоя индекса на диске
    D)Повторный запрос по условию видит новые чужие строки
    показать ответ и разбор
    +D)Повторный запрос по условию видит новые чужие строки

    // разбор: Фантом — повторный запрос с тем же условием (WHERE) возвращает новый набор строк, потому что другая транзакция вставила/удалила подходящие записи и закоммитила. Отличается от non-repeatable read (там меняется существующая строка). Убирается SERIALIZABLE; в PostgreSQL снапшот REPEATABLE READ тоже защищает от типичных фантомов.

  4. #transactions_isolation4 / 5
    Два воркера читают баланс, оба списывают — итог неверный. Чем закрыть гонку на уровне БД?
    A)Просто обернуть оба списания в transaction.atomic по отдельности
    B)Читать баланс из кэша Redis, чтобы не трогать базу лишний раз
    C)SELECT ... FOR UPDATE — заблокировать строку до конца транзакции
    D)Увеличить уровень изоляции до READ UNCOMMITTED для скорости
    показать ответ и разбор
    +C)SELECT ... FOR UPDATE — заблокировать строку до конца транзакции

    // разбор: SELECT ... FOR UPDATE берёт блокировку на прочитанные строки до конца транзакции: второй воркер ждёт на том же ряду, поэтому read-modify-write не наложатся. Альтернатива — оптимистичная блокировка через версию/условие в UPDATE. atomic обеспечивает атомарность, но не сериализует доступ к строке сам по себе.

  5. #transactions_isolation5 / 5
    Что гарантирует свойство атомарности транзакции (A в ACID)?
    A)Все операции транзакции применяются целиком или не применяются вовсе
    B)Данные транзакции доступны на чтение другим сразу же, ещё до её фиксации
    C)После фиксации данные переживут сбой и перезапуск сервера базы
    D)Транзакция выполняется быстрее набора отдельных запросов за счёт их объединения
    показать ответ и разбор
    +A)Все операции транзакции применяются целиком или не применяются вовсе

    // разбор: Атомарность (A в ACID): транзакция — неделимая единица, её операции либо применяются все вместе (commit), либо не применяются никакие (rollback при ошибке). Незавершённое промежуточное состояние наружу не попадает. Это то, что не даёт списать деньги, но не зачислить: обе операции внутри одной транзакции — или обе, или ни одной.

дальше

Теорию прочитали. Навык ставится повторением

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