Как работает Repeatable Read уровень изоляции транзакций в PostgreSQL?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Repeatable Read уровень изоляции в PostgreSQL
Иерархия уровней изоляции
SQL стандарт определяет 4 уровня изоляции (от слабого к сильному):
1. READ UNCOMMITTED — читай незакоммиченные данные (самый слабый)
2. READ COMMITTED — читай только закоммиченные (default в PostgreSQL)
3. REPEATABLE READ — в одной транзакции всегда читаешь одни данные
4. SERIALIZABLE — полная изоляция (самый сильный)
Что такое Repeatable Read
Repeatable Read — это уровень изоляции, при котором:
- Транзакция видит снимок данных, созданный на момент начала первого SELECT (не при начале транзакции!)
- В рамках одной транзакции одни и те же данные всегда читаются одинаково
- Другие параллельные транзакции не могут "испортить" данные внутри этой транзакции
Проблема без REPEATABLE READ (READ COMMITTED):
Транзакция 1 Транзакция 2 Данные в БД
SET TRANSACTION ISOLATION
LEVEL READ COMMITTED
SELECT * FROM account
WHERE id = 1
SET TRANSACTION ISOLATION
LEVEL READ COMMITTED
UPDATE account
SET balance = 500
WHERE id = 1
COMMIT balance = 500
SELECT * FROM account
WHERE id = 1
-- Видит balance = 500 ← ❌ Dirty read!
(хотя только что было 1000)
Сная проблема: "Non-repeatable read"
С REPEATABLE READ:
Транзакция 1 (REPEATABLE READ) Транзакция 2 Данные в БД
SET TRANSACTION ISOLATION
LEVEL REPEATABLE READ
BEGIN
SELECT * FROM account
WHERE id = 1 balance = 1000 (snapshot)
-- Видит balance = 1000
SET TRANSACTION ISOLATION
LEVEL REPEATABLE READ
BEGIN
UPDATE account
SET balance = 500
WHERE id = 1
COMMIT balance = 500
SELECT * FROM account
WHERE id = 1
-- ВСЁ ЕЩЁ видит balance = 1000 ← ✅ Согласованность!
(из snapshot'а)
COMMIT
Как работает Repeatable Read в PostgreSQL
1. MVCC (Multi-Version Concurrency Control)
PostgreSQL использует MVCC для реализации изоляции:
// В PostgreSQL каждая строка имеет версии
private static class Row {
private Long id;
private BigDecimal balance;
private Long xmin; // Transaction ID, который создал эту версию
private Long xmax; // Transaction ID, который удалил эту версию
}
// Пример истории одной строки:
// Version 1: xmin=100, xmax=200 (Transact 100 создал, 200 удалил)
// Version 2: xmin=200, xmax=300 (Transact 200 создал, 300 удалил)
// Version 3: xmin=300, xmax=∞ (Transact 300 создал, текущая версия)
// Когда транзакция начинается, PostgreSQL запоминает:
// - Свой xid (transaction ID)
// - Список всех активных транзакций на этот момент
2. Snapshot создаётся при первом SELECT
@Test
public void testRepeatableRead() throws SQLException {
// Подключение 1
Connection conn1 = dataSource.getConnection();
// Подключение 2
Connection conn2 = dataSource.getConnection();
// === Транзакция 1: REPEATABLE READ ===
conn1.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
conn1.setAutoCommit(false);
// Первый SELECT — создаёт snapshot
Statement stmt1 = conn1.createStatement();
ResultSet rs1 = stmt1.executeQuery("SELECT balance FROM account WHERE id = 1");
rs1.next();
BigDecimal balance1 = rs1.getBigDecimal("balance");
// balance1 = 1000
// SNAPSHOT создан в этот момент!
// === Транзакция 2: обновляет данные ===
conn2.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
conn2.setAutoCommit(false);
Statement stmt2 = conn2.createStatement();
stmt2.executeUpdate("UPDATE account SET balance = 500 WHERE id = 1");
conn2.commit(); // Коммитим обновление
// === Транзакция 1: читаем снова ===
rs1 = stmt1.executeQuery("SELECT balance FROM account WHERE id = 1");
rs1.next();
BigDecimal balance2 = rs1.getBigDecimal("balance");
// balance2 = 1000 (из snapshot'а, хотя в БД уже 500)
// ✅ Repeatable read работает!
assertEquals(balance1, balance2);
conn1.commit();
conn2.close();
}
3. Видимость данных
Постгрес решает для каждой версии: видна ли она для транзакции?
private boolean isVersionVisible(Row version, Transaction currentTx) {
// Пример:
// Версия: xmin=200, xmax=300
// Текущая транзакция: xid=250
// Если текущая транзакция создала эту версию
if (version.xmin == currentTx.xid) {
return true; // Видим свои изменения
}
// Если версия создана другой транзакцией
if (version.xmin != currentTx.xid) {
// Видима ли эта версия для нас?
if (version.xmin < currentTx.snapshotStartXid) {
// И создавшая её транзакция закоммичена
if (isCommitted(version.xmin)) {
return true; // Видим эту версию
}
}
}
// Версия удалена
if (version.xmax != Long.MAX_VALUE) {
if (version.xmax <= currentTx.snapshotStartXid) {
return false; // Версия удалена до нашего snapshot'а
}
}
return false;
}
Проблемы, которые решает Repeatable Read
1. Non-Repeatable Read (Неповторяемое чтение)
-- Транзакция 1 (READ COMMITTED) ❌
BEGIN;
SELECT balance FROM account WHERE id = 1; -- 1000
-- Ждём...
SELECT balance FROM account WHERE id = 1; -- 500! (изменилось)
COMMIT;
-- Транзакция 2
UPDATE account SET balance = 500 WHERE id = 1;
COMMIT;
-- === С REPEATABLE READ ✅ ===
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM account WHERE id = 1; -- 1000
-- Ждём...
SELECT balance FROM account WHERE id = 1; -- 1000! (одинаково)
COMMIT;
2. Phantom Read (Фантомное чтение)
Осторожно! REPEATABLE READ в PostgreSQL не полностью защищает от phantom read:
-- Транзакция 1 (REPEATABLE READ)
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT COUNT(*) FROM account WHERE balance > 1000; -- 5
-- Ждём...
SELECT COUNT(*) FROM account WHERE balance > 1000; -- 6! (добавилась новая)
COMMIT;
-- Транзакция 2
INSERT INTO account (balance) VALUES (2000);
COMMIT;
Для полной защиты используй SERIALIZABLE:
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT COUNT(*) FROM account WHERE balance > 1000; -- 5
-- Ждём...
SELECT COUNT(*) FROM account WHERE balance > 1000; -- 5! (одинаково)
COMMIT;
Сравнение уровней изоляции
Уровень Dirty Non-Rep Phantom Производ
Read ительность
──────────────────────────────────────────────────────
READ COMMITTED Нет Да Да Высокая
REPEATABLE READ Нет Нет Да Средняя
SERIALIZABLE Нет Нет Нет Низкая
Как использовать в Java
@Transactional(isolation = Isolation.REPEATABLE_READ)
public class AccountService {
@Autowired
private AccountRepository accountRepository;
public BigDecimal getBalance(Long accountId) {
// Первый запрос — создаст snapshot
Account account = accountRepository.findById(accountId).orElse(null);
BigDecimal balance1 = account.getBalance();
// Какая-то логика...
doSomeWork();
// Второй запрос — прочитает из того же snapshot'а
account = accountRepository.findById(accountId).orElse(null);
BigDecimal balance2 = account.getBalance();
// ✅ balance1 == balance2 (гарантировано)
assertEquals(balance1, balance2);
return balance1;
}
}
Когда использовать
✅ REPEATABLE READ используй когда:
- Нужна согласованность данных в пределах одной транзакции
- Выполняешь несколько SELECT'ов и ожидаешь одни же данные
- Делаешь сложные расчёты на основе прочитанных данных
- Транзакция работает долго
❌ Не используй если:
- Нужна максимальная производительность (READ COMMITTED быстрее)
- Приложение очень high-load (каждая уровень изоляции добавляет overhead)
На собеседовании
"REPEATABLE READ в PostgreSQL — это уровень изоляции, при котором транзакция видит снимок (snapshot) данных, созданный при первом SELECT.
Как работает:
- Транзакция начинается
- При первом SELECT создаётся snapshot с данными на этот момент
- При последующих SELECT'ах видны данные из того же snapshot'а
- Другие параллельные транзакции не могут "испортить" данные внутри этой транзакции
Проблемы, которые решает:
- Non-repeatable read: одни данные больше не изменяются внутри транзакции
Проблемы, которые НЕ решает:
- Phantom read: новые строки могут появиться (для этого нужен SERIALIZABLE)
Реализуется через MVCC (Multi-Version Concurrency Control) — несколько версий одной строки."