Що таке Repeatable Read?
На рівні Repeatable Read транзакція працює зі "знімком" даних, зробленим в момент першого запиту.
🟢 Junior Level
Repeatable Read — це рівень ізоляції, який гарантує: якщо транзакція прочитала дані, то всі наступні читання тих самих рядків повернуть той самий результат.
Основні властивості
- Брудне читання: Заборонено
- Неповторюване читання: Заборонено — дані не зміняться під час транзакції
- Фантомне читання: Залежить від СУБД (у MySQL заборонено, у стандарті можливе)
Простий приклад
Транзакція А: SELECT price FROM products WHERE id = 1; -- Повернув 100
-- Інша транзакція змінює ціну і робить COMMIT
Транзакція А: SELECT price FROM products WHERE id = 1; -- Все одно 100!
На рівні Repeatable Read транзакція працює зі “знімком” даних, зробленим в момент першого запиту.
Відмінність від Read Committed
- Read Committed: Кожен SELECT бачить останні закомічені дані
- Repeatable Read: Усі SELECTи бачать дані на момент початку транзакції
🟡 Middle Level
Механізм реалізації (MVCC)
На рівні Repeatable Read знімок (Snapshot) даних створюється один раз — в момент першого запиту в транзакції. Усі наступні запити бачать базу в тому стані, в якому вона була на момент знімка.
Відмінності між MySQL та PostgreSQL
Хоча назва однакова, реалізація фундаментально різниться:
MySQL (InnoDB)
- Це рівень ізоляції за замовчуванням
- Запобігає фантомному читанню через Next-Key Locking (Record Lock + Gap Lock):
Gap = проміжок між двома індексними записами. Gap Lock блокує вставку нових рядків у цей проміжок. Наприклад, якщо в індексі є id=5 та id=10, gap lock на (5,10) не дасть вставити id=7.
- Блокуються не лише рядки, а й проміжки між ними
- Поводиться майже як Serializable
PostgreSQL
- Не використовує блокування проміжків (Gap Locks)
- Якщо транзакція намагається змінити рядок, який закомітила інша транзакція після початку вашої — отримаєте помилку:
ERROR: could not serialize access due to concurrent update - Додаток має повторити транзакцію повністю
Сценарій використання
- Звіти: Формування звіту, де дані не повинні “плисти” в процесі
- Багатокрокові обчислення: Коли проміжні результати не повинні змінюватися
- Міграції даних: Копіювання даних у консистентному стані
Не використовуйте RR для: висококонкурентних записів (>100 writers на одні дані), довгих транзакцій (>30 сек) — ризик bloat та serialization failures.
Накладні витрати
- СУБД зберігає старі версії рядків довше (поки не завершиться найдовша транзакція RR)
- Може призвести до роздування бази (Table Bloat)
- Збільшує навантаження на процес VACUUM
Проблема Lost Update
Repeatable Read у PostgreSQL захищає від “втраченого оновлення”. Якщо два потоки одночасно змінюють один запис, другий отримає помилку серіалізації при commit-і.
🔴 Senior Level
MVCC Snapshot Lifecycle
PostgreSQL Snapshot Behavior
- Snapshot created at first read in transaction, not at BEGIN
- Snapshot includes:
xmin: oldest active transaction IDxmax: next transaction ID to be assignedxip_array: snapshot of all in-progress transactions
- All subsequent reads use this same snapshot
- Writes see their own changes immediately (own-XID visibility rule)
Tuple Visibility Under RR
// Simplified PostgreSQL visibility check
bool HeapTupleSatisfiesMVCC(HeapTuple tuple, Snapshot snapshot) {
if (tuple->xmin >= snapshot->xmax) return false;
if (tuple->xmax < snapshot->xmin) return false;
if (TransactionIdIsInProgress(tuple->xmin)) return false;
if (!TransactionIdDidCommit(tuple->xmin)) return false;
if (TransactionIdIsInProgress(tuple->xmax)) return true;
if (TransactionIdDidCommit(tuple->xmax)) return false;
return true;
}
MySQL Next-Key Locking — Internal Details
Next-Key Lock = Record Lock + Gap Lock
-- Index: id = 1, 5, 10, 15
SELECT * FROM t WHERE id = 10 FOR UPDATE;
This locks:
- The record with id=10 (record lock)
- The gap (5, 10) — prevents INSERT of id=6,7,8,9
- The gap (10, 15) — prevents INSERT of id=11,12,13,14
Why? Prevents phantom reads by blocking inserts that would match the query predicate.
Side effect: Higher deadlock probability, especially with range queries.
Serialization Failure Handling (PostgreSQL)
@Retryable(
value = {CannotSerializeTransactionException.class},
backoff = @Backoff(delay = 100, multiplier = 2, maxDelay = 1000),
maxAttempts = 3
)
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void updateBalance(Long accountId, BigDecimal amount) {
Account account = repo.findById(accountId);
account.setBalance(account.getBalance().add(amount));
repo.save(account);
}
Lost Update Prevention Deep Dive
Scenario under RR in PostgreSQL:
T1: BEGIN; T2: BEGIN;
T1: SELECT balance FROM acc WHERE id=1; -- бачить 100
T2: SELECT balance FROM acc WHERE id=1; -- бачить 100
T1: UPDATE acc SET balance = 90 WHERE id=1;
T1: COMMIT;
T2: UPDATE acc SET balance = 80 WHERE id=1;
-- T2 BLOCKS, waiting for T1
-- T1 committed, T2 detects conflict
-- ERROR: could not serialize access
PostgreSQL detects that T2 read a row that T1 modified after T2’s snapshot. T2 must retry.
Performance Impact Analysis
| Метрика | Read Committed | Repeatable Read |
|---|---|---|
| Створення snapshot | На оператор | На транзакцію |
| Зберігання старих версій | Короткочасне | До завершення найдовшої tx |
| Потенціал bloat | Низький | Середньо-Високий |
| Тиск на VACUUM | Низький | Високий |
| Помилки конфліктів | Відсутні | Можливі при записі |
| Ризик deadlock | Низький | Середній (MySQL) |
Production Recommendations
When to use Repeatable Read:
- Complex reports requiring self-consistency
- Multi-step business logic with invariants
- Batch processing where source data must not change
- When you need protection against lost updates without explicit locking
When to avoid:
- High-concurrency write-heavy workloads
- Long-running transactions (bloat risk)
- When retry logic is too complex for your use case
Tuning Tips:
- Keep RR transactions as short as possible
- Monitor
pg_stat_user_tables.n_dead_tupfor bloat - Set
statement_timeoutto prevent runaway transactions - Use
pg_stat_activityto detect long-running RR transactions
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- Repeatable Read гарантує: повторний SELECT тих самих рядків поверне той самий результат
- Забороняє dirty read та non-repeatable read; фантоми — залежить від СУБД
- PostgreSQL: snapshot створюється при першому запиті, один на всю транзакцію
- MySQL: дефолтний рівень, запобігає фантомам через Next-Key Locking (Gap Locks)
- PostgreSQL при конфлікті оновлень кидає помилку серіалізації — потрібен retry
- RR захищає від lost update у PostgreSQL — другий потік отримає serialization failure
Часті уточнюючі питання:
- Чому MySQL та PG по-різному обробляють фантоми на RR? — MySQL: Gap Locks, PG: MVCC snapshot
- Коли використовувати RR? — Звіти, багатокрокові обчислення, міграції даних
- Що таке Gap Lock? — Блокування проміжку між індексними записами, запобігає вставці
- Чим RR відрізняється від Serializable? — RR не запобігає write skew, Serializable — запобігає
Червоні прапорці (НЕ говорити):
- “RR в усіх СУБД однаковий” — MySQL та PG фундаментально різняться
- “RR = Serializable” — RR не запобігає всім аномаліям (write skew)
- “RR не впливає на продуктивність” — bloat від старих версій рядків, VACUUM pressure
Пов’язані теми:
- [[2. Які рівні ізоляції транзакцій існують]]
- [[6. Що таке Serializable]]
- [[10. Що таке втрачене оновлення (Lost Update)]]
- [[12. Який рівень ізоляції за замовчуванням в MySQL]]