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

Приведи пример изоляции repeatable read

1.8 Middle🔥 241 комментариев
#Docker, Kubernetes и DevOps#REST API и микросервисы

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

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

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

Приведи пример изоляции repeatable read

Repeatable Read — это уровень изоляции транзакций, который гарантирует, что данные, прочитанные в начале транзакции, остаются теми же при повторных чтениях, даже если другие транзакции меняют эти данные. Это один из четырех основных уровней изоляции транзакций в SQL.

Уровни изоляции транзакций

Уровень                 Dirty Read  Non-Repeatable Read  Phantom Read
────────────────────────────────────────────────────────────────────────
1. Read Uncommitted        ✓              ✓                  ✓
2. Read Committed          ✗              ✓                  ✓
3. Repeatable Read         ✗              ✗                  ✓
4. Serializable            ✗              ✗                  ✗

Repeatable Read защищает от:

  • Dirty Read — чтение незафиксированных данных ✓ Защита
  • Non-Repeatable Read — изменение данных между чтениями ✓ Защита
  • Phantom Read — появление новых строк (может быть в некоторых БД)

Проблема Non-Repeatable Read (без защиты)

Сценарий без Repeatable Read

Транзакция A                          Транзакция B
─────────────────────────────────────────────────────────
SET TRANSACTION ISOLATION LEVEL
READ COMMITTED;

BEGIN;

SELECT balance FROM accounts
WHERE id = 1;
-- Результат: 1000

                                     BEGIN;
                                     UPDATE accounts
                                     SET balance = 500
                                     WHERE id = 1;
                                     COMMIT;

SELECT balance FROM accounts
WHERE id = 1;
-- ПРОБЛЕМА: результат 500 (ИЗМЕНИЛОСЬ!)
-- Это Non-Repeatable Read

COMMIT;

Проблема: одна и та же строка была прочитана дважды с разными значениями!

Решение: Repeatable Read

Пример на PostgreSQL

-- Транзакция A
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;

SELECT balance FROM accounts WHERE id = 1;
-- Результат: 1000 (Первое чтение)

-- В этом момент Транзакция B меняет данные...
-- (см. ниже)

SELECT balance FROM accounts WHERE id = 1;
-- Результат: 1000 (ВСЕ ЕЩЕ 1000 - изменение видно не будет!)
-- Это REPEATABLE READ в действии

COMMIT;
-- Транзакция B (выполняется параллельно)
BEGIN;
UPDATE accounts SET balance = 500 WHERE id = 1;
COMMIT;

Результат: Транзакция A видит одно и то же значение (1000) несмотря на изменения в Транзакции B.

Практический пример на Java с Spring Data JPA

Конфигурация Isolation Level

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

@Service
public class AccountService {
    
    @Autowired
    private AccountRepository accountRepository;
    
    // Используем Repeatable Read
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void transferMoney(Long accountId, BigDecimal amount) {
        // Чтение 1
        Account account = accountRepository.findById(accountId)
            .orElseThrow(() -> new AccountNotFoundException(accountId));
        
        BigDecimal balance1 = account.getBalance();
        System.out.println("First read: " + balance1);  // Например: 1000
        
        // Имитируем обработку (другая транзакция может менять данные)
        processTransaction(account);
        
        // Чтение 2 (в REPEATABLE READ увидим то же значение)
        account = accountRepository.findById(accountId)
            .orElseThrow(() -> new AccountNotFoundException(accountId));
        
        BigDecimal balance2 = account.getBalance();
        System.out.println("Second read: " + balance2);  // Тоже 1000!
        
        // В Repeatable Read: balance1 == balance2
        // Не будет Non-Repeatable Read проблемы
    }
    
    private void processTransaction(Account account) {
        // Долгая обработка
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Детальный пример: конфликт сценариев

Сценарий: Перевод денег

// ИСХОДНЫЕ ДАННЫЕ
// Account с id=1: balance = 1000

public class TransferService {
    
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void checkAndTransfer(Long accountId, BigDecimal amount) {
        // === ТРАНЗАКЦИЯ A НАЧАЛО ===
        Account account = findAccount(accountId);
        
        // ЧТЕНИЕ 1: balance = 1000
        BigDecimal currentBalance = account.getBalance();
        System.out.println("[T.A] Прочитано: " + currentBalance);
        
        // === ЗДЕСЬ ТРАНЗАКЦИЯ B ВМЕШИВАЕТСЯ ===
        // B делает UPDATE accounts SET balance = 500 WHERE id = 1
        // === В REPEATABLE READ это не повлияет на A ===
        
        // ЧТЕНИЕ 2: все еще 1000 (повторяемое чтение)
        account = findAccount(accountId);
        BigDecimal retryBalance = account.getBalance();
        System.out.println("[T.A] Повторное чтение: " + retryBalance);
        
        // Проверка
        if (currentBalance.equals(retryBalance)) {
            System.out.println("✓ Repeatatable Read: значение не изменилось");
        }
        
        // Логика обработки
        if (currentBalance.compareTo(amount) >= 0) {
            account.setBalance(currentBalance.subtract(amount));
            save(account);
        }
    }
}

Как работает Repeatable Read внутри

В PostgreSQL:

-- Транзакция A
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;  -- Создаёт снимок (snapshot) состояния БД на этот момент

SELECT * FROM accounts WHERE id = 1;  -- Читает из снимка
-- Видит balance = 1000

-- Даже если в базе реально balance = 500, A видит 1000
-- потому что это было в снимке при начале транзакции

SELECT * FROM accounts WHERE id = 1;  -- Читает из того же снимка
-- Все ещё видит 1000

COMMIT;  -- Снимок больше не нужен

Механизм: MVCC (Multi-Version Concurrency Control)

Экземпляр 1 (версия от T.B):  balance = 500, version = 5
Экземпляр 2 (версия от T.A):  balance = 1000, version = 3 ← Видит А

Т.A читает с версией 3 → всегда видит 1000
Т.B читает свои 500 → видит версию 5

Отличие от других уровней

Read Committed (чувствителен к Non-Repeatable Read)

@Transactional(isolation = Isolation.READ_COMMITTED)
public void problem() {
    Account a = repo.findById(1L).get();  // balance = 1000
    
    // Другая транзакция обновляет
    // UPDATE accounts SET balance = 500 WHERE id = 1
    
    Account a2 = repo.findById(1L).get();  // balance = 500 (!)
    // ПРОБЛЕМА: разные значения
}

Repeatable Read (защищён)

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void noProblems() {
    Account a = repo.findById(1L).get();  // balance = 1000
    
    // Другая транзакция обновляет
    // UPDATE accounts SET balance = 500 WHERE id = 1
    
    Account a2 = repo.findById(1L).get();  // balance = 1000 (!) 
    // OK: одно и то же значение
}

Когда использовать Repeatable Read

Хорошие случаи:

// 1. Проверка-и-действие (check-then-act)
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void paymentFlow(Long accountId, BigDecimal amount) {
    Account account = getAccount(accountId);
    
    // Проверяем баланс (чтение 1)
    if (account.getBalance().compareTo(amount) >= 0) {
        // Обработка (может быть долгой)
        processPayment(amount);
        
        // Повторная проверка баланса (чтение 2)
        // В REPEATABLE READ увидим то же значение
        Account verified = getAccount(accountId);
        if (verified.getBalance().compareTo(amount) >= 0) {
            account.setBalance(account.getBalance().subtract(amount));
            save(account);
        }
    }
}

// 2. Вычисление на основе данных
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void generateReport(Long departmentId) {
    // Все чтения будут с одного состояния БД
    int count = countEmployees(departmentId);  // Чтение 1
    BigDecimal salary = calculateAvgSalary(departmentId);  // Чтение 2
    // Гарантированно согласованные данные
}

Когда это может быть проблемой:

// Может возникнуть Phantom Read при INSERT
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void vulnerableToPhantom() {
    // Чтение 1
    List<Order> orders1 = repo.findByStatus("PENDING");  // 5 заказов
    
    // Другая транзакция добавляет новый заказ
    // INSERT INTO orders (status) VALUES ('PENDING')
    
    // Чтение 2
    List<Order> orders2 = repo.findByStatus("PENDING");  // 6 заказов!
    // PHANTOM READ: новая строка появилась
}

Для защиты от Phantom Read используйте Serializable.

Заключение

Repeatable Read гарантирует, что данные, прочитанные в начале транзакции, остаются консистентными при повторных чтениях. Это достигается через моментальный снимок (snapshot) состояния БД благодаря MVCC. Этот уровень обеспечивает хороший баланс между безопасностью данных и производительностью, что делает его выбором по умолчанию во многих приложениях.