Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Repeatable-Read: уровень изоляции транзакций
REPEATABLE-READ — это один из четырёх стандартных уровней изоляции транзакций в SQL (согласно стандарту SQL-92). Он определяет, как транзакции видят друг друга и взаимодействуют с БД.
Четыре уровня изоляции (от слабого к сильному)
- READ-UNCOMMITTED — минимальная изоляция
- READ-COMMITTED — средняя изоляция (по умолчанию в большинстве БД)
- REPEATABLE-READ — высокая изоляция (по умолчанию в MySQL)
- SERIALIZABLE — максимальная изоляция
Зачем нужен REPEATABLE-READ
Проблема: в слабых уровнях изоляции возникают аномалии:
- Dirty reads — чтение незафиксированных данных
- Non-repeatable reads — повторное чтение даёт разные результаты
- Phantom reads — появление новых строк при повторном чтении
REPEATABLE-READ решает: если транзакция прочитала строку, другие транзакции не могут изменить эту строку до завершения первой транзакции.
Пример проблемы без REPEATABLE-READ
Уровень READ-COMMITTED:
Транзакция 1 Транзакция 2
─────────────────────────────────────────────
BEGIN;
SELECT balance FROM accounts
WHERE id = 1;
// Результат: 100
BEGIN;
UPDATE accounts
SET balance = 50
WHERE id = 1;
COMMIT;
SELECT balance FROM accounts
WHERE id = 1;
// Результат: 50 (изменилось!)
// Non-repeatable read!
COMMIT;
Проблема: одна и та же SELECT вернула разные результаты.
С REPEATABLE-READ уровнем
Транзакция 1 Транзакция 2
─────────────────────────────────────────────
BEGIN;
SELECT balance FROM accounts
WHERE id = 1;
// Результат: 100
// Строка заблокирована для чтения
BEGIN;
UPDATE accounts
SET balance = 50
WHERE id = 1;
// ЖДЁТ завершения Транзакции 1!
// Блокировка (lock wait)
SELECT balance FROM accounts
WHERE id = 1;
// Результат: 100 (не изменилось!)
// Гарантия repeatability
COMMIT;
// Теперь Транзакция 2 может обновить
Реализация в Java / Spring Boot
// Установка уровня изоляции через аннотацию
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
// Читаем счета
Account fromAccount = accountRepository.findById(fromId)
.orElseThrow();
Account toAccount = accountRepository.findById(toId)
.orElseThrow();
// Повторные чтения вернут те же значения
BigDecimal fromBalance1 = fromAccount.getBalance();
// ... бизнес-логика ...
// Это прочитает ТЕ ЖЕ ДАННЫЕ, что и выше
BigDecimal fromBalance2 = fromAccount.getBalance();
// fromBalance1 == fromBalance2 гарантировано!
// Обновление
fromAccount.setBalance(fromBalance2.subtract(amount));
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
}
}
Уровни изоляции в сравнении
| Уровень | Dirty Reads | Non-Repeatable | Phantom Reads | Производительность |
|---|---|---|---|---|
| READ-UNCOMMITTED | ✅ Да | ✅ Да | ✅ Да | ✅ Быстро |
| READ-COMMITTED | ❌ Нет | ✅ Да | ✅ Да | ✅ Хорошо |
| REPEATABLE-READ | ❌ Нет | ❌ Нет | ✅ Да | ⚠️ Медленнее |
| SERIALIZABLE | ❌ Нет | ❌ Нет | ❌ Нет | ❌ Медленно |
Phantom Reads в REPEATABLE-READ
Одна проблема остаётся — появление новых строк:
Транзакция 1 Транзакция 2
─────────────────────────────────────────────
BEGIN;
SELECT COUNT(*) FROM accounts
WHERE balance > 1000;
// Результат: 3
BEGIN;
INSERT INTO accounts
VALUES (10, 1500);
COMMIT;
SELECT COUNT(*) FROM accounts
WHERE balance > 1000;
// Результат: 4 (phantom read!)
// Новая строка появилась
COMMIT;
Для предотвращения phantom reads нужен SERIALIZABLE.
Использование в Spring Boot
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
@Query("SELECT a FROM Account a WHERE a.balance > ?1")
List<Account> findHighBalanceAccounts(BigDecimal minBalance);
}
@Service
public class ReportService {
@Autowired
private AccountRepository accountRepository;
// Используем REPEATABLE-READ для аккуратных отчётов
@Transactional(isolation = Isolation.REPEATABLE_READ)
public AccountsReport generateReport() {
List<Account> accounts = accountRepository.findHighBalanceAccounts(
BigDecimal.valueOf(1000)
);
// Все последующие чтения вернут те же данные
BigDecimal totalBalance = accounts.stream()
.map(Account::getBalance)
.reduce(BigDecimal.ZERO, BigDecimal::add);
int totalCount = accounts.size();
return new AccountsReport(totalBalance, totalCount);
}
}
Практические примеры использования
1. Переводы денег
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
// Гарантируем, что баланс не изменится
// между первым чтением и обновлением
Account from = findAccount(fromId);
if (from.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
}
Account to = findAccount(toId);
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
save(from);
save(to);
}
2. Проверка консистентности
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void validateInventory() {
// Читаем инвентарь
List<Item> items = itemRepository.findAll();
int totalItems = items.stream()
.mapToInt(Item::getQuantity)
.sum();
// Повторное чтение гарантирует те же данные
int totalItems2 = itemRepository.sumAllQuantities();
// totalItems == totalItems2 гарантировано
if (totalItems != totalItems2) {
throw new InventoryMismatchException();
}
}
3. Генерация отчётов
@Transactional(isolation = Isolation.REPEATABLE_READ)
public Report generateMonthlyReport(int month) {
// Гарантируем стабильные данные за месяц
List<Transaction> transactions =
findTransactionsByMonth(month);
// Все производные расчёты базируются на одних данных
BigDecimal income = calculateIncome(transactions);
BigDecimal expenses = calculateExpenses(transactions);
BigDecimal profit = income.subtract(expenses);
return new Report(income, expenses, profit);
}
Когда использовать REPEATABLE-READ
✅ Используй когда:
- Критична точность расчётов (финансы, аккаунтинг)
- Нужно несколько чтений одних данных
- Важна консистентность отчётов
- Работаешь с денежными операциями
- Обновляешь данные на основе предыдущих чтений
❌ Избегай когда:
- Простые операции чтения (не критично)
- Высокая нагрузка и performance важен
- Можешь обойтись READ-COMMITTED
Производительность
READ-UNCOMMITTED: быстро (но небезопасно)
READ-COMMITTED: хорошо (стандарт)
REPEATABLE-READ: медленнее (больше lock'ов)
SERIALIZABLE: очень медленно (максимум lock'ов)
Поведение в разных БД
PostgreSQL:
- Использует MVCC (Multi-Version Concurrency Control)
- REPEATABLE-READ на основе снимков
- Хорошая производительность
MySQL (InnoDB):
- REPEATABLE-READ по умолчанию
- Использует gap locks
- Может быть медленнее
Oracle:
- Использует Read Consistency
- Автоматическое управление версиями
Резюме
- REPEATABLE-READ гарантирует, что повторные чтения вернут одинаковые данные
- Решает проблему: non-repeatable reads и обновления на основе устаревших данных
- Не решает: phantom reads (используй SERIALIZABLE)
- Использование: для финансовых операций, отчётов, консистентности данных
- Компромисс: безопасность vs производительность
Уровень REPEATABLE-READ — это хороший баланс между безопасностью данных и производительностью для большинства приложений.