Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable

Рейтинг: 55.2% · 12 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable (вы здесь)
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 7. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable

Когда к базе данных одновременно лезут десятки сессий, встаёт вопрос: что одна транзакция видит из недоделанной работы другой и насколько они мешают друг другу. Уровень изоляции в postgresql - это ручка, которой вы регулируете компромисс между корректностью и параллелизмом. В этом уроке разберём три уровня, которые реально использует PostgreSQL (Read Committed, Repeatable Read, Serializable), какие аномалии каждый из них пропускает, как устроена защита Serializable через SSI и как выбрать уровень под конкретную задачу. Будем гонять конкурентные транзакции в двух сеансах psql и смотреть, кто что видит.

Изображение

Как это работает

Стандарт SQL описывает четыре уровня и определяет их через запреты аномалий: грязное чтение (dirty read - увидеть незакоммиченные данные), неповторяющееся чтение (non-repeatable read - перечитал строку, а она изменилась), фантом (phantom - перечитал набор по условию, а в нём прибавилось строк). PostgreSQL реализует изоляцию через MVCC и снимки данных (snapshot), поэтому ведёт себя строже стандарта. В частности, грязного чтения тут не бывает вообще, даже на самом слабом уровне. Уровень Read Uncommitted в PostgreSQL существует только на бумаге: запросить его можно, но работать он будет как Read Committed.

Read Committed - это уровень по умолчанию. На нём каждый оператор внутри транзакции видит свой свежий снимок: данные, закоммиченные на момент старта именно этого оператора. Поэтому два одинаковых SELECT в одной транзакции могут вернуть разное, если между ними кто-то успел зафиксировать изменение. Звучит как баг, но для большинства OLTP-нагрузок это разумный дефолт: вы всегда работаете с актуальными данными и редко держите долгие транзакции.

Repeatable Read поднимает планку: снимок берётся один раз, в момент первого оператора транзакции, и держится до конца. Вся транзакция видит базу как застывшую фотографию. Non-repeatable read и фантомы исчезают: что прочитали в начале, то и будете видеть. Здесь важная особенность PostgreSQL - его Repeatable Read давит фантомы полностью, тогда как стандарт этого не требует. Платой становится возможная ошибка сериализации: если ваша транзакция попытается изменить строку, которую после взятия снимка уже поменял и закоммитил кто-то другой, вы получите serialization_failure (код 40001) и транзакцию надо повторять.

Serializable - самый строгий уровень. Он гарантирует, что результат параллельного выполнения транзакций будет таким же, как если бы они шли строго по очереди, в каком-то порядке. Именно этот уровень ловит аномалию записи вразнобой, write skew, которую Repeatable Read пропускает. Классический write skew: две транзакции читают общее условие (например, на смене должен остаться хотя бы один дежурный), каждая видит двоих, каждая снимает с дежурства себя - по отдельности всё валидно, а вместе дежурных не осталось. Snapshot-изоляция этого не замечает, потому что транзакции не трогали одни и те же строки.

В PostgreSQL Serializable работает через SSI - Serializable Snapshot Isolation. Это не блокировки в лоб, а тот же MVCC-снимок плюс отслеживание опасных зависимостей чтения и записи между транзакциями. Когда движок видит цикл зависимостей, способный дать неэквивалентный последовательному результат, он откатывает одну из транзакций с ошибкой сериализации. Поэтому при Serializable приложение обязано уметь повторять упавшие транзакции. SSI берёт легковесные predicate-блокировки (SIReadLock), они не мешают чужой записи, а лишь фиксируют факт чтения для анализа конфликтов.

SQL и примеры

Посмотреть и задать уровень. Уровень фиксируется в начале транзакции и не меняется после первого запроса:

Код: Выделить всё

SHOW default_transaction_isolation;   -- read committed по умолчанию

BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) FROM bookings.flights WHERE status = 'Delayed';
COMMIT;
Демонстрация non-repeatable read на демобазе Авиаперевозки. Откройте два psql. В сеансе A читаем количество задержанных рейсов дважды в одной транзакции на уровне по умолчанию:

Код: Выделить всё

-- Сеанс A (Read Committed)
BEGIN;
SELECT count(*) FROM bookings.flights WHERE status = 'Delayed';  -- допустим, 5
-- здесь не коммитим, ждём сеанс B

Код: Выделить всё

-- Сеанс B
UPDATE bookings.flights SET status = 'Delayed'
WHERE flight_id = (SELECT flight_id FROM bookings.flights
                   WHERE status = 'Scheduled' LIMIT 1);
COMMIT;

Код: Выделить всё

-- Сеанс A продолжает
SELECT count(*) FROM bookings.flights WHERE status = 'Delayed';  -- уже 6
COMMIT;
Два одинаковых запроса в одной транзакции дали разный ответ - это и есть неповторяющееся чтение. Повторите тот же сценарий, добавив в сеансе A SET TRANSACTION ISOLATION LEVEL REPEATABLE READ сразу после BEGIN: оба SELECT вернут 5, потому что транзакция держит единый снимок.

Воспроизведём write skew на Serializable. Смоделируем правило бронирования места: на рейс нельзя продавать билет, если свободных мест в салоне меньше двух (грубая защита от овербукинга). Считаем занятые места по boarding_passes:

Код: Выделить всё

-- оба сеанса начинают так
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- проверяем, что свободных мест ещё хватает (читают оба)
SELECT count(*) AS taken
FROM bookings.boarding_passes bp
JOIN bookings.flights f ON f.flight_id = bp.flight_id
WHERE f.flight_no = 'PG0001' AND f.scheduled_departure::date = '2017-07-15';

-- каждый сеанс, увидев "мест ещё достаточно", вставляет свой посадочный
INSERT INTO bookings.boarding_passes(ticket_no, flight_id, boarding_no, seat_no)
VALUES ('0005432000999', 30625, 200, '1A');   -- во втором сеансе другой ticket_no/seat
COMMIT;
Оба прочитали одно и то же число и каждый по-своему правильно решил вставить. На Repeatable Read обе транзакции спокойно закоммитятся и нарушат правило. На Serializable вторая фиксация упадёт с ошибкой:

Код: Выделить всё

ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: ...
HINT:  The transaction might succeed if retried.
Это сигнал приложению повторить транзакцию. На повторе она уже увидит обновлённую картину и примет верное решение. Минимальный шаблон ретрая на psql-уровне - ловить SQLSTATE 40001 и крутить транзакцию заново (в коде это обычно цикл с несколькими попытками).

Полезно для отладки явно посмотреть текущий снимок и уровень:

Код: Выделить всё

BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT txid_current();              -- id транзакции (в PG 13+ есть pg_current_xact_id())
SHOW transaction_isolation;
COMMIT;
Частые грабли
  • Думают, что Read Committed даёт стабильную картину на всю транзакцию. Нет: снимок обновляется на каждом операторе, между двумя SELECT данные могут уехать.
  • Ставят SERIALIZABLE и забывают про обработку ошибки 40001. Без логики повтора транзакций приложение начнёт ронять пользователям случайные ошибки под нагрузкой.
  • Меняют уровень после первого запроса в транзакции. SET TRANSACTION ISOLATION LEVEL работает только до первого оператора, иначе ошибка.
  • Ждут, что Repeatable Read поймает write skew. Не поймает: snapshot-изоляция видит только конфликты по одним и тем же строкам, а write skew - это конфликт по логике, а не по строкам.
  • Путают serialization_failure с deadlock. Дедлок (40P01) - это циклическое ожидание блокировок, его движок разрывает сразу; ошибка сериализации (40001) - это вердикт SSI, оба лечатся повтором, но причины разные.
  • Полагаются на уровень изоляции вместо явных блокировок там, где нужна именно блокировка. SELECT ... FOR UPDATE и advisory locks решают часть задач дешевле, чем глобальный Serializable.
  • Для postgresql для 1с по привычке выкручивают изоляцию вверх. 1С сама управляет блокировками через свой менеджер; обычно достаточно Read Committed, а лишний уровень только добавит откатов.
Мини-лаба
  • Откройте два сеанса psql к своей базе (можно к демобазе bookings). В обоих выполните SHOW transaction_isolation и убедитесь, что это read committed.
  • В сеансе A: BEGIN; затем SELECT count(*) FROM bookings.seats WHERE aircraft_code = '773'; не коммитьте.
  • В сеансе B измените одну строку seats этого самолёта (например, поменяйте fare_conditions у одного места) и сделайте COMMIT.
  • В сеансе A повторите тот же SELECT и сравните результат - поймайте неповторяющееся чтение. Сделайте COMMIT.
  • Повторите шаги 2-4, но в сеансе A после BEGIN добавьте SET TRANSACTION ISOLATION LEVEL REPEATABLE READ. Убедитесь, что оба SELECT совпали.
  • Соберите write skew на SERIALIZABLE: в обоих сеансах прочитайте одно агрегатное условие, затем оба вставьте/обновите строку, нарушающую это условие вместе. Зафиксируйте появление ERROR 40001 во втором сеансе.
  • Напишите псевдокод ретрая: повторять транзакцию при SQLSTATE 40001 до 3 раз, иначе вернуть ошибку.
Контрольные вопросы
  • Почему в PostgreSQL не бывает грязного чтения даже на уровне Read Committed и как с этим связан MVCC?
  • Чем снимок данных на Read Committed отличается от снимка на Repeatable Read и в какой момент каждый из них берётся?
  • Что такое write skew, почему его пропускает Repeatable Read и как Serializable его ловит?
  • Как работает SSI и чем ошибка сериализации (40001) отличается от взаимоблокировки (40P01)?
  • Почему при использовании Serializable приложение обязано уметь повторять транзакции?
  • В каких случаях разумнее взять SELECT ... FOR UPDATE вместо повышения уровня изоляции до Serializable?
👍1 ❤️2 🔥4 😄 🤔2
Аватара пользователя
tornado1
Сообщения: 1
Зарегистрирован: 19 май 2026, 14:12

Re: Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable

Сообщение tornado1 »

Поймал 40001 на сериализуемом под нагрузкой, сначала думал что у меня дедлок. А это SSI откатывает. Ретрай в три попытки спас, спасибо за шаблон.
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
rryancey
Сообщения: 1
Зарегистрирован: 13 май 2026, 14:53

Re: Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable

Сообщение rryancey »

А правда что репитбл рид в постгресе фантомы тоже режет? В вузе учили что только серилайзебл. Проверил на flights двумя сеансами - да, снимок застывает целиком, фантомов нет.
👍3 ❤️1 🔥 😄 🤔
Ответить
← Предыдущая глава
Транзакции и ACID
Следующая глава →
Соединения таблиц (JOIN)

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: уровни изоляции транзакций и блокировки в postgresql

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя