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

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

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

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

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

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

Проблемы транзакций и уровни изоляции

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

Проблемы конкурентности без изоляции

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

Проблема: Транзакция читает данные, которые ещё не закоммичены другой транзакцией.

-- Сценарий
Транзакция 1 (USER_A):
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- (Баланс счёта 1 изменился на 900, но не закоммичено)

Транзакция 2 (USER_B):
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- Читает 900 (грязное значение!)

Транзакция 1 (USER_A):
ROLLBACK; -- Откатывает изменения! Баланс обратно 1000

Транзакция 2 (USER_B):
-- USER_B построил решение на основе 900, которое больше не существует!
COMMIT;
// Пример в коде
public class DirtyReadExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        
        // Транзакция 1: Изменить баланс
        executor.submit(() -> {
            try (Connection conn = getConnection()) {
                conn.setAutoCommit(false);
                conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
                
                Statement stmt = conn.createStatement();
                stmt.executeUpdate("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
                
                Thread.sleep(2000); // Имитируем задержку
                conn.rollback(); // Откатываем
            }
        });
        
        // Транзакция 2: Прочитать баланс (грязное чтение!)
        executor.submit(() -> {
            try {
                Thread.sleep(1000); // Даём транзакции 1 начать
                try (Connection conn = getConnection()) {
                    conn.setAutoCommit(false);
                    conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
                    
                    Statement stmt = conn.createStatement();
                    ResultSet rs = stmt.executeQuery("SELECT balance FROM accounts WHERE id = 1");
                    rs.next();
                    System.out.println("Баланс (грязное чтение): " + rs.getInt("balance")); // Может быть 900!
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }
}

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

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

-- Сценарий
Транзакция 1 (USER_A):
BEGIN;
SELECT price FROM products WHERE id = 1; -- Читает 99.99
-- Делает некоторые вычисления

Транзакция 2 (USER_B):
BEGIN;
UPDATE products SET price = 79.99 WHERE id = 1;
COMMIT;

Транзакция 1 (USER_A):
SELECT price FROM products WHERE id = 1; -- Читает 79.99 (отличается от первого!))
COMMIT;
// Пример: Расчёт скидки на товар
public void calculateDiscount(int productId) {
    try (Connection conn = getConnection()) {
        conn.setAutoCommit(false);
        conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
        
        // Первое чтение цены
        double price1 = getPrice(conn, productId); // 100.0
        
        // Работаем с ценой
        double discount = calculateDiscountBasedOnPrice(price1);
        
        // Второе чтение той же цены
        double price2 = getPrice(conn, productId); // 50.0 (изменилась!)
        
        // Несоответствие!
        if (price1 != price2) {
            System.out.println("Цена изменилась между двумя чтениями!");
        }
        
        conn.commit();
    }
}

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

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

-- Сценарий
Транзакция 1 (USER_A):
BEGIN;
SELECT COUNT(*) FROM orders WHERE total > 1000; -- Результат: 5

Транзакция 2 (USER_B):
BEGIN;
INSERT INTO orders (user_id, total) VALUES (123, 2000);
COMMIT;

Транзакция 1 (USER_A):
SELECT COUNT(*) FROM orders WHERE total > 1000; -- Результат: 6 (фантомная запись!)
COMMIT;
// Пример: Отчёт о статистике
public void generateReport() {
    try (Connection conn = getConnection()) {
        conn.setAutoCommit(false);
        conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
        
        // Первый запрос
        long count1 = countOrdersWithHighTotal(conn); // 5
        
        // Работаем с данными
        double totalRevenue = calculateTotalRevenue(conn);
        
        // Второй запрос того же условия
        long count2 = countOrdersWithHighTotal(conn); // 6 (добавилась новая строка!)
        
        // Несоответствие!
        if (count1 != count2) {
            System.out.println("Количество записей изменилось!");
        }
        
        conn.commit();
    }
}

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

Проблема: Две транзакции читают одно значение, изменяют его и записывают обратно, но одно изменение теряется.

-- Сценарий
Транзакция 1: чтение
SELECT balance FROM accounts WHERE id = 1; -- 1000

Транзакция 2: чтение
SELECT balance FROM accounts WHERE id = 1; -- 1000

Транзакция 1: обновление
UPDATE accounts SET balance = 1000 - 100 = 900 WHERE id = 1;
COMMIT;

Транзакция 2: обновление
UPDATE accounts SET balance = 1000 + 50 = 1050 WHERE id = 1;
COMMIT; -- Потеряно обновление транзакции 1!

-- Правильный результат должен быть 950 (1000 - 100 + 50)
// Проблемный код
@Service
public class BankService {
    public void transferMoney(int fromId, int toId, double amount) {
        // Транзакция 1 и 2 могут выполниться одновременно
        
        // Чтение
        double balance = getBalance(fromId);
        
        // Обновление
        if (balance >= amount) {
            updateBalance(fromId, balance - amount);
        }
    }
}

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

SQL стандарт определяет 4 уровня изоляции, каждый решает определённые проблемы:

Уровень                  | Dirty | Non-Rep | Phantom
                         | Read  | Read    | Read
------------------------------------------------------
READ_UNCOMMITTED         |  ✓    |   ✓     |   ✓
READ_COMMITTED           |  ✗    |   ✓     |   ✓
REPEATABLE_READ          |  ✗    |   ✗     |   ✓
SERIALIZABLE             |  ✗    |   ✗     |   ✗

✓ = проблема может возникнуть
✗ = проблема не возникнет

1. READ_UNCOMMITTED (Самый слабый)

Что позволяет:

  • Чтение незакоммиченных изменений других транзакций
Connection conn = getConnection();
conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);

try {
    conn.setAutoCommit(false);
    
    // Можешь прочитать "грязные" данные
    Statement stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT * FROM accounts");
    
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
}

Проблемы:

  • Dirty read: ✓
  • Non-repeatable read: ✓
  • Phantom read: ✓

Использование:

  • Почти никогда (разве что очень специфичные случаи)
  • Когда нужна максимальная производительность и точность не важна

2. READ_COMMITTED (По умолчанию в большинстве БД)

Что позволяет:

  • Читать только закоммиченные данные
  • Но другие транзакции могут обновить данные между чтениями
Connection conn = getConnection();
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

try {
    conn.setAutoCommit(false);
    
    // Транзакция 1
    double balance1 = getBalance(conn, 1); // 1000
    
    // Другая транзакция может обновить баланс здесь
    
    double balance2 = getBalance(conn, 1); // 950 (изменилось!)
    
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
}

Решённые проблемы:

  • Dirty read: ✗ (защищено)

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

  • Non-repeatable read: ✓
  • Phantom read: ✓

Использование:

  • Стандартный выбор для большинства приложений
  • Хороший баланс между консистентностью и производительностью

3. REPEATABLE_READ (Стандарт в MySQL)

Что позволяет:

  • Гарантирует, что данные не изменятся внутри транзакции
  • Но новые строки могут быть добавлены (phantom read)
Connection conn = getConnection();
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);

try {
    conn.setAutoCommit(false);
    
    // Первое чтение
    double balance1 = getBalance(conn, 1); // 1000
    
    // Другие транзакции НЕ могут изменить это значение
    // но могут добавить новые строки
    
    double balance2 = getBalance(conn, 1); // 1000 (гарантировано!)
    
    // Но этот запрос может вернуть разное количество строк
    long count1 = countAllAccounts(conn); // 100
    long count2 = countAllAccounts(conn); // 101 (новая учётная запись!
    
    conn.commit();
} catch (SQLException e) {
    conn.rollback();
}

Решённые проблемы:

  • Dirty read: ✗ (защищено)
  • Non-repeatable read: ✗ (защищено)

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

  • Phantom read: ✓

Использование:

  • Когда важна консистентность внутри одной транзакции
  • MySQL использует это по умолчанию

4. SERIALIZABLE (Самый сильный)

Что позволяет:

  • Полная изоляция, как если бы транзакции выполнялись последовательно
Connection conn = getConnection();
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);

try {
    conn.setAutoCommit(false);
    
    // Транзакция 1 выполняется так, как будто других транзакций нет
    double balance1 = getBalance(conn, 1); // 1000
    balance1 -= 100;
    updateBalance(conn, 1, balance1); // 900
    
    // Все другие транзакции ждут
    
    double balance2 = getBalance(conn, 1); // Гарантировано 900
    long count = countAllAccounts(conn); // Гарантирован одинаковый результат
    
    conn.commit(); // Только тогда другие транзакции могут продолжить
} catch (SQLException e) {
    conn.rollback();
}

Решённые проблемы:

  • Dirty read: ✗ (защищено)
  • Non-repeatable read: ✗ (защищено)
  • Phantom read: ✗ (защищено)

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

  • Производительность: потому что всё работает последовательно

Использование:

  • Редко, когда абсолютно необходима полная изоляция
  • Финансовые системы с очень критичными операциями
  • Может привести к deadlock'ам из-за блокировок

Как работает изоляция внутри

Механизм блокировок (Locking)

// READ_COMMITTED обычно реализуется с помощью блокировок для записи
@Service
public class AccountService {
    public void transfer(int from, int to, double amount) {
        // 1. Получает WRITE LOCK на счёт from
        // 2. Читает баланс
        // 3. Вычисляет новый баланс
        // 4. Обновляет запись
        // 5. Отпускает WRITE LOCK
        
        // Другие транзакции могут читать (если не в процессе обновления)
        // но не могут писать
    }
}

MVCC (Multi-Version Concurrency Control)

// PostgreSQL и некоторые другие БД используют MVCC
// Вместо блокировок хранят несколько версий данных

Транзакция 1: BEGIN;
Транзакция 2: BEGIN;
Транзакция 2: UPDATE account SET balance = 950;
Транзакция 2: COMMIT; // Версия 2

Транзакция 1: SELECT balance; // Видит версию 1 (950)
// Видит старую версию, чтобы избежать блокировок!

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

// В Spring Boot / JPA

// 1. Стандартный выбор (READ_COMMITTED)
@Transactional
public void normalOperation() {
    // Используется уровень БД по умолчанию
}

// 2. Когда нужна повторяемость чтений
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void reportGeneration() {
    // Гарантируется, что данные не изменятся
}

// 3. Для критичных финансовых операций
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transferMoney(int from, int to, double amount) {
    // Полная изоляция
}

// 4. Если нужна максимальная производительность (редко)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void analyticsQuery() {
    // Приблизительные данные для аналитики
}

Выводы

Уровни изоляции решают реальные проблемы:

  1. READ_UNCOMMITTED: Почти не используется
  2. READ_COMMITTED: Стандартный выбор (баланс производительности и консистентности)
  3. REPEATABLE_READ: Когда важны повторяющиеся чтения (MySQL default)
  4. SERIALIZABLE: Когда нужна полная изоляция (редко)

Правило выбора:

  • Начни с READ_COMMITTED (по умолчанию)
  • Если возникают проблемы — повышай уровень
  • Помни, что выше уровень = ниже производительность
  • Используй пессимистичные блокировки (SELECT FOR UPDATE) для критичных операций
  • Проверь, действительно ли нужна более высокая изоляция или можно решить проблему другим способом
Какие проблемы транзакции решают уровни изоляции | PrepBro