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

Какие аномалии изоляций транзакций не могут случаться

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

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

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

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

Какие аномалии изоляций транзакций не могут случаться

В реляционных БД существуют уровни изоляции транзакций, которые предотвращают различные аномалии. Рассмотрю, какие аномалии возможны и какие нет при разных уровнях.

Основные аномалии транзакций

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

// Одна транзакция читает неподтвержденные (грязные) данные от другой

// Транзакция 1 (не подтверждена)
UPDATE users SET balance = 1000 WHERE id = 1;
// balance = 1000 (но ROLLBACK еще не произошел)

// Транзакция 2 (читает грязное значение)
SELECT balance FROM users WHERE id = 1;  // Видит 1000

// Транзакция 1 откатывается
ROLLBACK;  // balance вернулся к 500

// Транзакция 2 видела 1000, но это не реально!

Предотвращается: READ COMMITTED и выше

2. Non-Repeatable Read (неповторяемое чтение)

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

// Транзакция 1
BEGIN;
SELECT balance FROM users WHERE id = 1;  // 500

// Транзакция 2 (выполняется между запросами)
UPDATE users SET balance = 600 WHERE id = 1;
COMMIT;

// Транзакция 1 (повторное чтение)
SELECT balance FROM users WHERE id = 1;  // 600 (изменилось!)
COMMIT;

// Данные изменились внутри одной транзакции

Предотвращается: REPEATABLE READ и выше

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

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

// Транзакция 1
BEGIN;
SELECT COUNT(*) FROM users WHERE age > 30;  // 5 строк

// Транзакция 2 (выполняется между запросами)
INSERT INTO users (name, age) VALUES ('Bob', 35);
COMMIT;

// Транзакция 1 (повторный запрос)
SELECT COUNT(*) FROM users WHERE age > 30;  // 6 строк (фантом!)
COMMIT;

// Новая строка появилась внутри транзакции

Предотвращается: SERIALIZABLE

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

// Две транзакции обновляют один и тот же объект

// Транзакция 1: balance = 1000
UPDATE users SET balance = balance + 100 WHERE id = 1;
// balance = 1100

// Транзакция 2: тоже видит balance = 1000
UPDATE users SET balance = balance + 50 WHERE id = 1;
// balance = 1050 (перезаписывает изменения транзакции 1!)

// Итог: balance = 1050, но должно быть 1150
// +100 потеряно!

Предотвращается: REPEATABLE READ и выше (при правильной блокировке)

Уровни изоляции в SQL

-- 1. READ UNCOMMITTED (самый низкий уровень)
-- Предотвращает: ничего
-- Позволяет: Dirty Read, Non-Repeatable Read, Phantom Read
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- 2. READ COMMITTED (по умолчанию в большинстве БД)
-- Предотвращает: Dirty Read
-- Позволяет: Non-Repeatable Read, Phantom Read
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 3. REPEATABLE READ (по умолчанию в MySQL)
-- Предотвращает: Dirty Read, Non-Repeatable Read
-- Позволяет: Phantom Read
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 4. SERIALIZABLE (самый высокий уровень)
-- Предотвращает: все аномалии
-- Позволяет: ничего (но медленнее всех)
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Java примеры с Spring

// Управление уровнем изоляции в Spring
@Service
public class UserService {
    
    // READ COMMITTED (по умолчанию)
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void updateBalance(Long userId, BigDecimal amount) {
        User user = userRepository.findById(userId).orElseThrow();
        user.setBalance(user.getBalance().add(amount));
        userRepository.save(user);
    }
    
    // REPEATABLE READ для критичных операций
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void transferMoney(Long from, Long to, BigDecimal amount) {
        User fromUser = userRepository.findById(from).orElseThrow();
        User toUser = userRepository.findById(to).orElseThrow();
        
        if (fromUser.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException();
        }
        
        fromUser.setBalance(fromUser.getBalance().subtract(amount));
        toUser.setBalance(toUser.getBalance().add(amount));
        
        userRepository.saveAll(Arrays.asList(fromUser, toUser));
    }
    
    // SERIALIZABLE для очень критичных операций
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void processPayment(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        // Гарантирует полную сериализацию
    }
}

Таблица предотвращения аномалий

УровеньDirty ReadNon-RepeatablePhantomLost Update
READ UNCOMMITTED❌ Нет❌ Нет❌ Нет❌ Нет
READ COMMITTED✅ Да❌ Нет❌ Нет🔶 Частично
REPEATABLE READ✅ Да✅ Да❌ Нет✅ Да
SERIALIZABLE✅ Да✅ Да✅ Да✅ Да

Какие аномалии НЕ МОГУТ случаться при разных уровнях

READ UNCOMMITTED:
- НЕ защищает ничего
- Все аномалии ВОЗМОЖНЫ

READ COMMITTED:
- Dirty Read НЕ МОЖЕТ случиться ✅
- Non-Repeatable Read МОЖЕТ случиться ❌
- Phantom Read МОЖЕТ случиться ❌

REPEATABLE READ:
- Dirty Read НЕ МОЖЕТ случиться ✅
- Non-Repeatable Read НЕ МОЖЕТ случиться ✅
- Phantom Read МОЖЕТ случиться ❌ (в стандарте SQL)
- В некоторых БД (MySQL) Phantom Read тоже предотвращается

SERIALIZABLE:
- Ничего не МОЖЕТ случиться ✅✅✅
- Все аномалии полностью предотвращены

PostgreSQL и MySQL особенности

-- PostgreSQL: REPEATABLE READ уже блокирует phantom reads
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- Phantom read НЕ МОЖЕТ случиться (используется MVCC)

-- MySQL: REPEATABLE READ уже блокирует phantom reads
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- Phantom read НЕ МОЖЕТ случиться (gap locking)

-- Обе БД обрабатывают phantom read по-разному,
-- но оба безопасны на REPEATABLE READ

Рекомендации

// 1. Для большинства операций: READ COMMITTED (по умолчанию)
@Transactional(isolation = Isolation.READ_COMMITTED)
public void normalOperation() { ... }

// 2. Для финансовых операций: REPEATABLE READ
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transferMoney() { ... }

// 3. Для очень критичных: SERIALIZABLE (но медленно!)
@Transactional(isolation = Isolation.SERIALIZABLE)
public void veryImportantOperation() { ... }

// 4. Используй SELECT FOR UPDATE для явной блокировки
@Query("SELECT u FROM User u WHERE u.id = :id FOR UPDATE")
User findByIdForUpdate(@Param("id") Long id);

Правильный выбор уровня изоляции — критичен для безопасности и производительности.

Какие аномалии изоляций транзакций не могут случаться | PrepBro