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

Для чего нужен каждый уровень изоляции БД?

2.7 Senior🔥 201 комментариев
#Базы данных и SQL

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

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

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

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

Введение

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

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

1. READ UNCOMMITTED (Чтение незафиксированных данных)

Назначение: Максимальная производительность за счет консистентности.

Проблемы:

  • Dirty Read — транзакция может читать данные, которые еще не зафиксированы другой транзакцией
// Транзакция A
setAutoCommit(false);
PreparedStatement stmt = conn.prepareStatement("UPDATE accounts SET balance = balance + 1000 WHERE id = 1");
stmt.executeUpdate();
// Не выполняем commit

// Транзакция B (с READ UNCOMMITTED)
Connection connB = dataSource.getConnection();
connB.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
PreparedStatement readStmt = connB.prepareStatement("SELECT balance FROM accounts WHERE id = 1");
ResultSet rs = readStmt.executeQuery();
// Видит увеличенный баланс, хотя трансакция A еще не выполнила commit!

Когда использовать: Практически никогда. Используется только для отчетов, не критичных к точности.

2. READ COMMITTED (Чтение зафиксированных данных)

Назначение: Баланс между консистентностью и производительностью.

Решаемые проблемы:

  • Dirty Read не возможен — читаются только зафиксированные данные

Оставшиеся проблемы:

  • Non-Repeatable Read — одна строка может измениться между двумя чтениями в одной транзакции
// Транзакция A
Connection connA = dataSource.getConnection();
connA.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

PreparedStatement select1 = connA.prepareStatement("SELECT balance FROM accounts WHERE id = 1");
ResultSet rs1 = select1.executeQuery();
rs1.next();
int balance1 = rs1.getInt("balance"); // Значение: 1000

// Здесь другая транзакция B изменяет и коммитит запись

PreparedStatement select2 = connA.prepareStatement("SELECT balance FROM accounts WHERE id = 1");
ResultSet rs2 = select2.executeQuery();
rs2.next();
int balance2 = rs2.getInt("balance"); // Значение: 2000 (изменилось!)

Когда использовать: Это уровень по умолчанию в большинстве БД. Подходит для большинства приложений.

3. REPEATABLE READ (Повторяемое чтение)

Назначение: Гарантирует, что данные внутри транзакции не изменяются.

Решаемые проблемы:

  • Non-Repeatable Read невозможен — строка заблокирована для других транзакций

Оставшиеся проблемы:

  • Phantom Read — новые строки, вставленные другой транзакцией, видны в результатах
// Транзакция A
Connection connA = dataSource.getConnection();
connA.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);

PreparedStatement select1 = connA.prepareStatement("SELECT COUNT(*) FROM orders WHERE status = active");
ResultSet rs1 = select1.executeQuery();
rs1.next();
int count1 = rs1.getInt(1); // Значение: 10

// Другая транзакция B вставляет новую строку и коммитит

PreparedStatement select2 = connA.prepareStatement("SELECT COUNT(*) FROM orders WHERE status = active");
ResultSet rs2 = select2.executeQuery();
rs2.next();
int count2 = rs2.getInt(1); // Значение: 11 (новая строка появилась!)

Когда использовать: Когда критично, чтобы строки не менялись в рамках транзакции, но добавление новых строк допустимо.

4. SERIALIZABLE (Сериализуемость)

Назначение: Максимальная консистентность. Транзакции выполняются так, как если бы они были одна за другой.

Решаемые проблемы:

  • Phantom Read невозможен — вся таблица заблокирована
  • Все проблемы предыдущих уровней решены

Недостаток:

  • Значительное снижение производительности из-за блокировок
// Транзакция A
Connection connA = dataSource.getConnection();
connA.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);

PreparedStatement select1 = connA.prepareStatement("SELECT COUNT(*) FROM orders WHERE status = active");
ResultSet rs1 = select1.executeQuery();
rs1.next();
int count1 = rs1.getInt(1);

// Другие транзакции ЖДУТ, пока эта не завершится
// И не могут вставлять новые строки в таблицу orders

Когда использовать: Только для критичных финансовых операций или когда консистентность важнее производительности.

Таблица сравнения проблем

УровеньDirty ReadNon-Repeatable ReadPhantom Read
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

Практический пример в Java

@Service
public class AccountService {
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId).orElseThrow();
        Account to = accountRepository.findById(toId).orElseThrow();
        
        from.setBalance(from.getBalance().subtract(amount));
        to.setBalance(to.getBalance().add(amount));
    }
    
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void criticalPaymentOperation(Long accountId) {
        // Для очень критичных операций используем SERIALIZABLE
    }
}

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

  1. Начните с READ COMMITTED — это хороший компромисс для большинства приложений
  2. Используйте REPEATABLE READ, если нужна консистентность строк
  3. Избегайте SERIALIZABLE без необходимости — это сильно влияет на производительность
  4. Продумайте логику приложения — часто правильно разработанная бизнес-логика может компенсировать более низкий уровень изоляции
  5. Мониторьте deadlocks — при высоких уровнях изоляции они становятся более вероятными