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

Какие проблемы решают каждый уровень изоляции

2.7 Senior🔥 131 комментариев
#Базы данных и SQL#Многопоточность

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

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

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

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

Уровни изоляции (isolation levels) в базах данных решают разные проблемы конкурентного доступа к данным. Это баланс между производительностью и консистентностью. Разберём каждый.

Проблемы без изоляции

Dirty Read (грязное чтение)

Транзакция читает не закоммиченные изменения другой транзакции:

-- Транзакция 1
BEGIN TRANSACTION;
UPDATE accounts SET balance = 5000 WHERE id = 1;
-- Баланс изменён на 5000, но не закоммичен

-- Транзакция 2 (в это же время)
BEGIN TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  -- Видит 5000 (не закоммичено!)

-- Транзакция 1 откатывается
ROLLBACK;
-- Теперь баланс обратно к 4000

-- Транзакция 2 работает с неверным значением 5000 (ghost data)
COMMIT;

Non-Repeatable Read (нестабильное чтение)

Транзакция читает одни и те же данные дважды, получая разные результаты:

-- Транзакция 1
BEGIN TRANSACTION;
SELECT name FROM users WHERE id = 1;  -- Результат: "Alice"

-- Транзакция 2 (в это же время)
BEGIN TRANSACTION;
UPDATE users SET name = "Alicia" WHERE id = 1;
COMMIT;

-- Транзакция 1 (продолжается)
SELECT name FROM users WHERE id = 1;  -- Результат: "Alicia" (изменилось!)
COMMIT;

Phantom Read (фантомное чтение)

Транзакция читает набор строк дважды, а во второй раз появляются/исчезают новые строки:

-- Транзакция 1
BEGIN TRANSACTION;
SELECT COUNT(*) FROM users WHERE age > 18;  -- Результат: 100

-- Транзакция 2 (в это же время)
BEGIN TRANSACTION;
INSERT INTO users (age) VALUES (25);
COMMIT;  -- Добавили пользователя

-- Транзакция 1 (продолжается)
SELECT COUNT(*) FROM users WHERE age > 18;  -- Результат: 101 (фантом!)
COMMIT;

Lost Update (потеря обновления)

Одновременные обновления одного поля перезаписывают друг друга:

Начальный баланс: 1000

-- Транзакция 1
READ balance = 1000
UPDATE balance = 1000 - 100 = 900

-- Транзакция 2
READ balance = 1000
UPDATE balance = 1000 - 50 = 950

-- Результат: 950 вместо 850 (100 потеряны)

УРОВЕНЬ 1: READ UNCOMMITTED (Читать не закоммиченное)

Что решает

Не решает практически ничего - это самый низкий уровень.

Проблемы, которые могут возникнуть

Dirty Read         ❌ МОЖЕТ БЫТЬ
Non-Repeatable     ❌ МОЖЕТ БЫТЬ
Phantom Read       ❌ МОЖЕТ БЫТЬ
Lost Update        ❌ МОЖЕТ БЫТЬ

Пример в PostgreSQL

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- Транзакция может читать грязные данные
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- Может быть неверно
COMMIT;

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

Никогда в production. Только для отчётов, где точность не критична.

УРОВЕНЬ 2: READ COMMITTED (Читать закоммиченное)

Что решает

Dirty Read         ✅ РЕШАЕТ - читаем только закоммиченные данные
Non-Repeatable     ❌ МОЖЕТ БЫТЬ
Phantom Read       ❌ МОЖЕТ БЫТЬ
Lost Update        ❌ МОЖЕТ БЫТЬ

Как работает

-- Транзакция 1
BEGIN;
UPDATE accounts SET balance = 500 WHERE id = 1;
-- Блокируем эту строку

-- Транзакция 2 (одновременно)
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- Ждёт коммита T1
COMMIT;

-- Транзакция 1
COMMIT;  -- Теперь T2 может прочитать 500

Проблема: Non-Repeatable Read

@Transactional(isolation = Isolation.READ_COMMITTED)
public void problematicRead() {
    // Чтение 1
    User user = repository.findById(1);
    System.out.println(user.getAge());  // 25
    
    // Другой процесс обновил пользователя
    // UPDATE users SET age = 26 WHERE id = 1; COMMIT;
    
    // Чтение 2 (в одной транзакции!)
    user = repository.findById(1);
    System.out.println(user.getAge());  // 26 (изменилось!)
}

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

Это уровень по умолчанию в большинстве БД (MySQL, PostgreSQL). Подходит для большинства приложений.

УРОВЕНЬ 3: REPEATABLE READ (Повторяемое чтение)

Что решает

Dirty Read         ✅ РЕШАЕТ
Non-Repeatable     ✅ РЕШАЕТ - данные не меняются внутри транзакции
Phantom Read       ❌ МОЖЕТ БЫТЬ
Lost Update        ❌ МОЖЕТ БЫТЬ (в некоторых БД)

Как работает

-- Транзакция 1
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT age FROM users WHERE id = 1;  -- 25

-- Транзакция 2 пытается обновить
UPDATE users SET age = 26 WHERE id = 1;
COMMIT;

-- Транзакция 1 (продолжается)
SELECT age FROM users WHERE id = 1;  -- Всё ещё 25!
-- Изменения T2 не видны (блокировка или snapshot)
COMMIT;

Проблема: Phantom Read

-- Транзакция 1
BEGIN REPEATABLE READ;
SELECT COUNT(*) FROM users WHERE age > 18;  -- 100

-- Транзакция 2
INSERT INTO users (age) VALUES (25);
COMMIT;

-- Транзакция 1
SELECT COUNT(*) FROM users WHERE age > 18;  -- 101 (phantom!)

Java/Hibernate

@Service
public class UserService {
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void transferMoney() {
        User user = repository.findById(1);
        double balance = user.getBalance();
        
        // Даже если другой процесс обновит баланс
        // Мы в этой транзакции видим старое значение
        
        doSomeThing();
        
        User sameUser = repository.findById(1);
        assertEquals(balance, sameUser.getBalance());  // Гарантированно true
    }
}

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

Когда нужна стабильность данных внутри транзакции (финансовые операции, отчёты).

УРОВЕНЬ 4: SERIALIZABLE (Сериализуемо)

Что решает

Dirty Read         ✅ РЕШАЕТ
Non-Repeatable     ✅ РЕШАЕТ
Phantom Read       ✅ РЕШАЕТ
Lost Update        ✅ РЕШАЕТ

Как работает

Транзакции выполняются как будто последовательно (одна за другой):

-- Транзакция 1
BEGIN SERIALIZABLE;
SELECT COUNT(*) FROM users WHERE age > 18;  -- 100

-- Транзакция 2 ЖДЁТ
BEGIN SERIALIZABLE;
-- Не может даже начать изменения

-- Транзакция 1
COMMIT;  -- Теперь T2 может выполняться

-- Транзакция 2
SELECT COUNT(*) FROM users WHERE age > 18;  -- 101 (после T1)
COMMIT;

Стоимость: Deadlock'и

@Transactional(isolation = Isolation.SERIALIZABLE)
public void transfer() {
    // Может быть serialization conflict
    // PostgreSQL: "could not serialize access"
    // MySQL: "Lock wait timeout exceeded"
}

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

Очень редко. Только когда абсолютно нужна консистентность и готовы платить производительностью.

Сравнительная таблица

УровеньDirty ReadNon-RepPhantomLost UpdateПроизводDeadlock
READ_UNCOMMITTED✅ Макс✅ Низкий
READ_COMMITTED✅ Хорошо✅ Низкий
REPEATABLE_READ⚠️ Медленнее⚠️ Средний
SERIALIZABLE❌ Минимум❌ Макс

Практические примеры

1. Финансовая транзакция (высокий уровень)

@Service
public class BankService {
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepo.findByIdForUpdate(fromId);
        Account to = accountRepo.findByIdForUpdate(toId);
        
        // SELECT FOR UPDATE блокирует строки
        // Гарантирует, что никто не изменит между нашими операциями
        
        if (from.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException();
        }
        
        from.withdraw(amount);
        to.deposit(amount);
        
        accountRepo.save(from);
        accountRepo.save(to);
    }
}

2. Отчёт (низкий уровень)

@Service
public class ReportService {
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public ReportData generateReport(LocalDate date) {
        // Для отчётов точность менее критична
        // Немного устаревшие данные - нормально
        
        long totalSales = reportRepo.getTotalSalesForDate(date);
        long totalCustomers = reportRepo.getTotalCustomersForDate(date);
        
        return new ReportData(totalSales, totalCustomers);
    }
}

3. SELECT FOR UPDATE (явная блокировка)

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u WHERE u.id = :id")
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    User findByIdWithLock(@Param("id") Long id);
}

// Использование
@Transactional
public void updateUser(Long id) {
    User user = repository.findByIdWithLock(id);
    // Никто другой не может обновить этого пользователя
    user.setName("New Name");
}

Рекомендации по выбору

По типу операции

  • READ_COMMITTED (по умолчанию)

    • Веб-приложения
    • CRUD операции
    • API endpoints
  • REPEATABLE_READ

    • Финансовые операции
    • Критичные обновления
    • Операции с инвентарём
  • SERIALIZABLE

    • Аудит данных
    • Критичные бизнес-операции
    • Готовы к медленности

Вывод

Уровни изоляции - это компромисс между консистентностью и производительностью:

  1. READ_COMMITTED - стандарт для 90% случаев
  2. REPEATABLE_READ - когда нужна стабильность данных в транзакции
  3. Используйте SELECT FOR UPDATE для явной блокировки вместо повышения уровня
  4. SERIALIZABLE редко нужен - проверьте логику приложения сначала
Какие проблемы решают каждый уровень изоляции | PrepBro