11.1 Уровень изоляции - это политика снимка
Стандарт SQL описывает четыре уровня изоляции через аномалии, которые они запрещают. PostgreSQL реализует их строже стандарта, и реально различимы три уровня. Отличает их одно - когда берётся снимок (см. snapshot):
- Read Committed - новый снимок на каждый оператор;
- Repeatable Read - один снимок на всю транзакцию;
- Serializable - тот же снимок плюс проверка зависимостей.
Грязного чтения (увидеть незакоммиченное чужое) в PostgreSQL не бывает ни на одном уровне - это особенность реализации. Полный разбор - в isolation-levels.
11.2 Какие аномалии где возможны
| Аномалия | Read Committed | Repeatable Read | Serializable |
|---|---|---|---|
| грязное чтение | нет | нет | нет |
| неповторяющееся чтение | да | нет | нет |
| фантомное чтение | да | нет | нет |
| аномалия сериализации | да | да | нет |
Чем выше уровень, тем больше аномалий отсечено и тем дороже. Неповторяющееся и фантомное чтение уходят уже на Repeatable Read, потому что снимок заморожен. А самая хитрая - аномалия сериализации - проходит даже на Repeatable Read и ловится только на Serializable.
11.3 Неповторяющееся чтение на Read Committed
Воспроизведём аномалию на уровне по умолчанию. Сессия A читает бронь дважды, между чтениями сессия B её меняет и коммитит.
-- сессия A (Read Committed по умолчанию)
SELECT total FROM bookings WHERE book_ref = '000002'; -- 102.00
-- сессия B
UPDATE bookings SET total = 500 WHERE book_ref = '000002';
COMMIT;
-- сессия A, повторное чтение в той же транзакции
SELECT total FROM bookings WHERE book_ref = '000002'; -- 500.00 !
Два одинаковых запроса в одной транзакции дали разный ответ. Это и есть неповторяющееся чтение: между ними сессия A взяла новый снимок (на Read Committed снимок берётся на каждый оператор) и увидела свежий коммит сессии B.
11.4 Repeatable Read замораживает картину
Тот же сценарий, но сессия A работает на Repeatable Read:
-- сессия A
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT total FROM bookings WHERE book_ref = '000002'; -- 102.00
-- сессия B меняет и коммитит, как раньше
-- сессия A, повторное чтение
SELECT total FROM bookings WHERE book_ref = '000002'; -- по-прежнему 102.00
Картина не дрогнула. Снимок взялся один раз в начале транзакции и держится до конца, поэтому коммит сессии B в него не просочился. Повторное чтение всегда даёт тот же ответ. За это и ценят Repeatable Read: отчёт на нём видит согласованный срез, что бы ни творилось вокруг.
11.5 Serializable ловит перекос записи
Repeatable Read убирает неповторяющиеся и фантомные чтения, но пропускает аномалию посложнее - перекос записи (write skew). Классика: двое дежурных, каждый снимает с дежурства себя, видя, что второй ещё на смене. По отдельности оба решения корректны, вместе - дежурных не осталось.
Воспроизведём на Serializable. Обе сессии видят, что на дежурстве двое, и каждая снимает своего:
-- сессия A -- сессия B
BEGIN ISOLATION LEVEL SERIALIZABLE; BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT count(*) FROM oncall SELECT count(*) FROM oncall
WHERE on_call; -- 2 WHERE on_call; -- 2
UPDATE oncall SET on_call=false UPDATE oncall SET on_call=false
WHERE name='alice'; WHERE name='bob';
COMMIT; -- успех COMMIT;
Сессия A коммитится спокойно. А сессия B при коммите получает:
ERROR: could not serialize access due to read/write dependencies among transactions
DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt.
HINT: The transaction might succeed if retried.
Перекос пойман. В итоге на дежурстве остался один - данные целы. Serializable увидел, что обе транзакции читали то, что меняла другая, и пожертвовал второй. Как именно он это отслеживает (предикатные блокировки SSI) - в serializable-ssi.
11.6 Цена за строгость: ошибка 40001
На Repeatable Read и Serializable конфликт не приводит к ожиданию -
транзакция откатывается с кодом 40001. Сообщения разные, причина
общая: «эту параллельность нельзя свести к последовательному
выполнению».
| Уровень | Сообщение при конфликте |
|---|---|
| Repeatable Read | could not serialize access due to concurrent update |
| Serializable | could not serialize access due to read/write dependencies |
Это штатный сигнал, а не сбой. Кто записал первым - выиграл, остальные
получают 40001 и должны повторить транзакцию. Поэтому приложение на
высоких уровнях изоляции обязано уметь повторять транзакцию по этому
коду.
11.7 Подводный камень: высокий уровень без повтора
Главная ошибка с высокими уровнями изоляции - включить Serializable и
забыть про повтор. Тогда первая же конкурентная нагрузка начнёт сыпать
пользователям ошибки 40001 как фатальные, хотя их надо было молча
повторить.
Serializable снимает с разработчика ручные блокировки SELECT FOR UPDATE и даёт писать код так, будто транзакции идут по очереди. Но
взамен требует одного: цикла повтора по 40001. Без него высокий
уровень изоляции из защиты превращается в источник случайных ошибок.
Правило простое: повышаешь уровень - добавляй повтор.
Уроки в sandbox
lab-11.1. Аномалии на разных уровнях
Воспроизведи неповторяющееся чтение, заморозь его на Repeatable Read и поймай ошибку сериализации на Serializable. Перед каждым шагом предсказывай: повторится ли чтение, пройдёт ли коммит.
В сессии A (Read Committed) прочитай
SELECT total FROM bookings WHERE book_ref='000002';и запомни значение.В сессии B измени и зафиксируй:
UPDATE bookings SET total=500 WHERE book_ref='000002'; COMMIT;.Прочитай в сессии A ещё раз - значение изменилось: это неповторяющееся чтение на Read Committed.
Создай таблицу дежурных:
CREATE TABLE oncall(name text, on_call boolean); INSERT INTO oncall VALUES ('alice',true),('bob',true);.В обеих сессиях открой
BEGIN ISOLATION LEVEL SERIALIZABLE;, прочитайSELECT count(*) FROM oncall WHERE on_call;(обе увидят 2), и сними каждая своего: A - alice, B - bob.Закоммить сессию A (пройдёт), затем сессию B - предскажи и поймай ошибку 40001; проверь, что на дежурстве остался ровно один.
sandbox с автопроверкой - открыть в песочнице
Резюме
- PostgreSQL различает три уровня изоляции; грязного чтения нет ни на одном.
- Read Committed берёт снимок на каждый оператор - возможны неповторяющееся и фантомное чтение.
- Repeatable Read замораживает снимок на всю транзакцию - эти аномалии исчезают.
- Serializable добавляет проверку зависимостей и ловит перекос записи, которого RR не видит.
- Конфликт на высоких уровнях даёт не ожидание, а откат с кодом 40001 - сигнал повторить транзакцию.
- Включив Serializable, обязательно добавь цикл повтора по 40001, иначе он сыплет ошибками.
Контрольные вопросы
Почему в PostgreSQL не бывает грязного чтения даже на самом низком уровне?
Показать ответ
Потому что так устроена реализация многоверсионности: транзакция видит только зафиксированные версии. Незакоммиченную чужую версию правило видимости отвергает на любом уровне изоляции. Стандарт SQL допускает грязное чтение на Read Uncommitted, но в PostgreSQL этот уровень ведёт себя как Read Committed - грязного чтения не происходит.
Чем отличается момент взятия снимка на Read Committed и Repeatable Read?
Показать ответ
На Read Committed новый снимок берётся на каждый оператор, поэтому повторное чтение в одной транзакции может увидеть свежие чужие коммиты - это и есть неповторяющееся чтение. На Repeatable Read снимок берётся один раз в начале транзакции и держится до конца, поэтому повторное чтение всегда даёт тот же результат. Разница в политике обновления снимка и определяет, какие аномалии возможны.
Что такое перекос записи и почему Repeatable Read его не ловит?
Показать ответ
Перекос записи - аномалия, где две транзакции читают пересекающиеся данные и каждая меняет то, что прочитала другая; по отдельности обе корректны, вместе нарушают инвариант (например, оба дежурных снялись со смены). Repeatable Read не ловит его, потому что каждая транзакция работает на своём замороженном снимке и не видит, что другая меняет прочитанное. Конфликта обновления одной строки тут нет - есть скрытая зависимость через чтение, которую отслеживает только Serializable.
Что значит ошибка 40001 и как на неё реагировать?
Показать ответ
40001 - ошибка сериализации: PostgreSQL обнаружил, что данную параллельность нельзя свести к последовательному выполнению, и откатил одну из транзакций. Это не сбой, а штатный сигнал. Реагировать нужно повтором: приложение должно поймать код 40001 и выполнить транзакцию заново. На Repeatable Read это конфликт обновления, на Serializable - зависимость чтения-записи, но реакция одна.
Какую обязанность накладывает переход на Serializable?
Показать ответ
Обязанность повторять транзакции по ошибке 40001. Serializable не блокирует и не ждёт - при опасной зависимости он откатывает транзакцию. Если приложение не ловит 40001 и не повторяет транзакцию, под конкурентной нагрузкой пользователи получат поток ошибок. Поэтому повышение уровня изоляции до Serializable всегда идёт в паре с циклом повтора.