Приведи пример изоляции repeatable read
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Приведи пример изоляции 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. Этот уровень обеспечивает хороший баланс между безопасностью данных и производительностью, что делает его выбором по умолчанию во многих приложениях.