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

Приведи пример когда возникает Repeatable Read

1.2 Junior🔥 151 комментариев
#Soft Skills и карьера

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

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

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

Repeatable Read: уровень изоляции транзакций

Repeatable Read — это один из четырёх стандартных уровней изоляции транзакций в SQL (определены в ACID). Это важная концепция при работе с многопользовательскими БД.

Что такое Repeatable Read?

Repeatable Read гарантирует, что если одна транзакция прочитала строку один раз, она прочитает ту же версию этой строки при повторном чтении в рамках одной транзакции. При этом могут появляться новые строки (phantom reads), если их вставить другие транзакции.

Иерархия уровней изоляции

1. READ UNCOMMITTED  — можешь читать грязные данные
2. READ COMMITTED    — можешь читать только committed данные
3. REPEATABLE READ   — одна строка = одна версия в транзакции
4. SERIALIZABLE      — абсолютная изоляция (как последовательное выполнение)

Практический пример: перевод денег между счётами

Представим, что работаем с таблицей счётов:

CREATE TABLE accounts (
    id BIGINT PRIMARY KEY,
    balance DECIMAL(10, 2)
);

INSERT INTO accounts VALUES (1, 1000.00), (2, 500.00);

Сценарий: Транзакция с Repeatable Read

Транзакция 1 (Java код через Spring/Hibernate):

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transferMoney() {
    // Т1: Читаем баланс счёта #1
    Account account1 = accountRepository.findById(1L);
    System.out.println("Баланс счёта 1: " + account1.getBalance()); // 1000.00
    
    // Задержка, чтобы дать другой транзакции время на изменение
    Thread.sleep(2000);
    
    // Т1: Ещё раз читаем баланс счёта #1
    account1 = accountRepository.findById(1L);
    System.out.println("Баланс счёта 1 после задержки: " + account1.getBalance()); 
    // ГАРАНТИРОВАННО: 1000.00 (не изменился!)
    
    // Пытаемся перевести деньги
    account1.setBalance(account1.getBalance() - 100);
    accountRepository.save(account1);
}

Транзакция 2 (выполняется одновременно из другого потока):

public void updateBalance() {
    // Т2: Изменяем баланс счёта #1
    Account account1 = accountRepository.findById(1L);
    account1.setBalance(800.00); // Уменьшили баланс
    accountRepository.save(account1);
}

Что произойдёт?

Время  | Транзакция 1                           | Транзакция 2
-------|----------------------------------------|------------------------
10:00  | BEGIN REPEATABLE READ                  |
10:01  | SELECT balance WHERE id=1 → 1000       |
10:02  |                                        | BEGIN READ COMMITTED
10:03  |                                        | UPDATE accounts SET balance=800
10:04  |                                        | COMMIT (изменения видны)
10:05  | [задержка 2 сек]                       |
10:06  | SELECT balance WHERE id=1 → 1000       | ← REPEATABLE READ!
       |                                        | Т1 не видит изменения Т2
10:07  | UPDATE accounts SET balance=900        |
10:08  | COMMIT                                 |

Чем это отличается от READ COMMITTED?

При READ COMMITTED Т1 увидела бы изменение:

@Transactional(isolation = Isolation.READ_COMMITTED)
public void transferMoneyReadCommitted() {
    Account account1 = accountRepository.findById(1L);
    System.out.println("Баланс: " + account1.getBalance()); // 1000.00
    
    Thread.sleep(2000); // Другая транзакция изменила на 800
    
    account1 = accountRepository.findById(1L);
    System.out.println("Баланс: " + account1.getBalance()); // 800.00 ← видим изменение!
}

Когда используется Repeatable Read?

1. Финансовые операции — нужна консистентность в рамках одной операции

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void calculateAccountInterest(Long accountId) {
    // Читаем баланс
    BigDecimal balance = getAccountBalance(accountId);
    
    // Проверяем условия
    if (balance.compareTo(BigDecimal.valueOf(10000)) > 0) {
        // Добавляем бонус (баланс не должен измениться между проверкой и начислением)
        addBonus(accountId, 100);
    }
}

2. Генерирование отчётов — нужна стабильная версия данных

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void generateMonthlyReport() {
    List<Transaction> transactions = getAllTransactions();
    BigDecimal totalAmount = calculateTotal(transactions);
    // Гарантия: durante отчета данные не изменятся
}

3. Проверка условий перед действием — нужна консистентность

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void withdrawMoney(Long accountId, BigDecimal amount) {
    // Проверяем баланс
    BigDecimal balance = getBalance(accountId);
    
    // Баланс гарантированно такой же при следующем чтении
    if (balance.compareTo(amount) >= 0) {
        withdraw(accountId, amount);
    }
}

Phantom Read проблема

Однако Repeatable Read не решает Phantom Read — когда в результат запроса добавляются новые строки:

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void countAccounts() {
    int count1 = accountRepository.count(); // 10 счётов
    
    Thread.sleep(2000); // Другая транзакция добавила счёт
    
    int count2 = accountRepository.count(); // 11 счётов!
    // Phantom read: новая строка появилась
}

Для полной изоляции нужен SERIALIZABLE.

Таблица сравнения

ПроблемаREAD COMMITTEDREPEATABLE READSERIALIZABLE
Dirty Read
Non-Repeatable Read
Phantom Read
ПроизводительностьЛучшеСреднеХуже

Spring/Hibernate практика

import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void businessLogic() {
    // Код здесь выполнится с уровнем изоляции REPEATABLE_READ
}

Вывод: Repeatable Read гарантирует консистентность одной строки в рамках транзакции, но не защищает от появления новых строк (Phantom Read). Это хороший компромисс между консистентностью и производительностью.

Приведи пример когда возникает Repeatable Read | PrepBro