← Назад к вопросам

Как работает Repeatable Read уровень изоляции транзакций в PostgreSQL?

2.7 Senior🔥 101 комментариев
#Базы данных и SQL

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# 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.

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

  1. Транзакция начинается
  2. При первом SELECT создаётся snapshot с данными на этот момент
  3. При последующих SELECT'ах видны данные из того же snapshot'а
  4. Другие параллельные транзакции не могут "испортить" данные внутри этой транзакции

Проблемы, которые решает:

  • Non-repeatable read: одни данные больше не изменяются внутри транзакции

Проблемы, которые НЕ решает:

  • Phantom read: новые строки могут появиться (для этого нужен SERIALIZABLE)

Реализуется через MVCC (Multi-Version Concurrency Control) — несколько версий одной строки."

Как работает Repeatable Read уровень изоляции транзакций в PostgreSQL? | PrepBro