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

Зачем нужен repeatable-read?

1.8 Middle🔥 121 комментариев
#Базы данных и SQL

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

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

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

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

REPEATABLE-READ — это один из четырёх стандартных уровней изоляции транзакций в SQL (согласно стандарту SQL-92). Он определяет, как транзакции видят друг друга и взаимодействуют с БД.

Четыре уровня изоляции (от слабого к сильному)

  1. READ-UNCOMMITTED — минимальная изоляция
  2. READ-COMMITTED — средняя изоляция (по умолчанию в большинстве БД)
  3. REPEATABLE-READ — высокая изоляция (по умолчанию в MySQL)
  4. 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 ReadsNon-RepeatablePhantom 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 — это хороший баланс между безопасностью данных и производительностью для большинства приложений.

Зачем нужен repeatable-read? | PrepBro