Приведи пример когда возникает Repeatable Read
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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 COMMITTED | REPEATABLE READ | SERIALIZABLE |
|---|---|---|---|
| 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). Это хороший компромисс между консистентностью и производительностью.