Опирается на правила:
PG-IS-001…PG-IS-082из PostgreSQL Style Guide → раздел Уровни изоляции.
Важно знать
- PG поддерживает 3 уровня:
READ COMMITTED(default),REPEATABLE READ(snapshot),SERIALIZABLE(SSI).READ UNCOMMITTEDне реализован — работает как RC.- PG RC строже стандарта — dirty read невозможен через MVCC.
- PG RR строже стандарта — snapshot isolation, phantom предотвращён.
- Только SERIALIZABLE ловит write skew (классика: «хотя бы один врач на смене»).
- Дефолт RC в 95% случаев — non-repeatable и phantom в OLTP редко проблема.
- 40001 serialization_failure на RR/SERIALIZABLE — нужен
@Retryable.@Transactional(readOnly = true)— для read-replica routing.idle_in_transaction_session_timeout: 30sв проде.
PostgreSQL даёт три уровня изоляции. Это не «больше = лучше» — каждый компромисс. UCP формулирует алгоритм выбора.
Аномалии
| Аномалия | Что |
|---|---|
| Dirty read | Видим uncommitted чужой TX |
| Non-repeatable read | Перечитал строку — значение изменилось |
| Phantom read | Перечитал WHERE — пришли новые строки |
| Serialization anomaly | Параллельные TX нарушают инвариант, который каждая соблюдает (write skew) |
Три уровня PG
| Уровень | Dirty | Non-repeat | Phantom | Serialization |
|---|---|---|---|---|
READ UNCOMMITTED (= RC в PG) | — | разрешает | разрешает | разрешает |
READ COMMITTED (default) | — | разрешает | разрешает | разрешает |
REPEATABLE READ (snapshot) | — | — | — | разрешает |
SERIALIZABLE | — | — | — | — |
PG-IS-001: уровни — компромисс. Выбирай минимальный под аномалии в твоём сценарии.
READ COMMITTED — дефолт
PG-IS-010..012: каждый SELECT видит самое последнее committed состояние на момент своего запуска.
TX1: BEGIN;
TX1: SELECT price FROM product WHERE id=1; -- 100
TX2: UPDATE product SET price = 120 WHERE id=1; COMMIT;
TX1: SELECT price FROM product WHERE id=1; -- 120 (non-repeatable!)
Non-repeatable и phantom разрешены. В OLTP редко проблема — обычно строку читают один раз.
Когда хватает RC:
- Простой CRUD.
- API-endpoints с одним чтением.
- Read-modify-write с
SELECT FOR UPDATE.
REPEATABLE READ — snapshot
PG-IS-020..023: snapshot на момент первого запроса в транзакции.
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) FROM orders WHERE status='NEW'; -- 100
-- (другая TX вставила новые orders и закоммитила)
SELECT count(*) FROM orders WHERE status='NEW'; -- всё ещё 100
COMMIT;
Когда оправдан:
- Длинный отчёт по нескольким таблицам с консистентным срезом.
pg_dump(использует RR).- Сложный bulk-перенос данных.
40001 на UPDATE изменённой после snapshot строки:
TX1 (RR): SELECT price FROM product WHERE id=1; -- 100
TX2 (RC): UPDATE product SET price=120 ... COMMIT;
TX1: UPDATE product SET price=110 WHERE id=1;
ERROR: 40001 — could not serialize access
Java должен делать retry. На bulk-операциях RR может стать SLA-проблемой.
SERIALIZABLE — full isolation
PG-IS-030..034: результат параллельных TX = последовательное выполнение.
Write skew пример
Инвариант: «всегда хотя бы один врач на смене». 2 врача on-call.
TX1 (RR): SELECT count(*) FROM doctors WHERE on_call=true; -- 2
TX1: -- «можно уйти, остаётся хотя бы 1»
TX1: UPDATE doctors SET on_call=false WHERE id=1;
TX2 (RR): SELECT count(*) FROM doctors WHERE on_call=true; -- 2
TX2: UPDATE doctors SET on_call=false WHERE id=2;
TX2: COMMIT;
TX1: COMMIT;
-- инвариант нарушен: 0 врачей
Под SERIALIZABLE одна из TX упадёт с 40001.
Когда нужен:
- Сложные инварианты, которые нельзя выразить через CHECK или FOR UPDATE.
- Финансовые расчёты с множественными правилами.
Дорого:
- Predicate locks для отслеживания зависимостей.
- % rollback-ов растёт под нагрузкой.
PG-IS-034: на большинстве OLTP — не оправдан. Дешевле — SELECT FOR UPDATE + ручной CHECK, оставив RC.
Spring
PG-IS-040..042:
@Transactional(isolation = Isolation.SERIALIZABLE)
public void releaseDoctorFromShift(long doctorId) {
var onCallCount = doctorRepository.countByOnCallTrue();
if (onCallCount <= 1) {
throw new LastDoctorOnShiftException();
}
doctorRepository.setOnCallFalse(doctorId);
}
READ_COMMITTED — дефолт, не указывай явно.
Retry для RR/SERIALIZABLE:
@Retryable(
retryFor = CannotSerializeTransactionException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 50, multiplier = 2)
)
@Transactional(isolation = Isolation.SERIALIZABLE)
public void doWork() { ... }
Алгоритм выбора
PG-IS-050..051:
- Read-modify-write одной строки? → RC +
SELECT FOR UPDATE. - Read-only отчёт по нескольким таблицам с консистентным срезом? → RR.
- Длинный bulk-перенос? → RR (read) или RC с явным батчингом.
- Сложный инвариант на нескольких строках, нельзя выразить через FOR UPDATE? → SERIALIZABLE + retry.
- Деньги/счета? → RC + FOR UPDATE (упорядоченные локи).
Сомнения → дефолт RC. Поднятие уровня без понимания конкретной аномалии — карго-культ.
Read-only
PG-IS-060..061:
@Transactional(readOnly = true)
public OrderSummary handle(GetOrderQuery query) { ... }
Эффект:
- HikariCP может направить на read-replica (если настроено).
- jOOQ/JPA пропускает flush.
- На PG — никакого effect автоматически.
Для эффекта на PG: SET TRANSACTION READ ONLY или provider_disables_autocommit=true.
idle_in_transaction_session_timeout
PG-IS-070:
-- на проде
idle_in_transaction_session_timeout = 30000 -- 30s
Автоматически убивает забытые транзакции. Длинная TX почти всегда баг.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
SERIALIZABLE на каждом методе | PG-IS-080 | минимальный нужный |
| RR на коротком read-modify-write | PG-IS-081 | RC + SELECT FOR UPDATE |
| Поднимать isolation вместо CHECK constraint | PG-IS-082 | declarative constraint |
Isolation.READ_COMMITTED явно (= default) | PG-IS-041 | не указывать |
RR/SERIALIZABLE без @Retryable | PG-IS-042 | CannotSerializeTransactionException |
idle_in_transaction_session_timeout отключён | PG-IS-070 | 30s минимум |
Куда дальше
- PG → Уровни изоляции — нормативные формулировки.
- Блокировки и jOOQ — FOR UPDATE дополнение к RC.
- Spring @Transactional — нюансы.
- HikariCP — read-replica routing.
- WAL — длинные TX и WAL.
- Distributed → idempotency — concurrent write protection.