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

В чем разница между фантомным чтением и неповторяющимся чтением?

1.2 Junior🔥 131 комментариев
#Основы Java

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

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

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

# Фантомное чтение vs Неповторяющееся чтение в SQL

Введение

Это две разные проблемы параллельности в базах данных, возникающие при определённых уровнях изоляции транзакций. Понимание различия критично для выбора правильного уровня изоляции.

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

Java определяет 4 уровня (java.sql.Connection):

// От наименьшей изоляции к наибольшей
Connection.TRANSACTION_READ_UNCOMMITTED;  // 0
Connection.TRANSACTION_READ_COMMITTED;    // 1
Connection.TRANSACTION_REPEATABLE_READ;   // 2
Connection.TRANSACTION_SERIALIZABLE;      // 4

Неповторяющееся чтение (Non-Repeatable Read)

Неповторяющееся чтение — это когда одна и та же строка при повторном чтении имеет другое значение в рамках одной транзакции.

Сценарий проблемы

Транзакция 1 (T1)               Транзакция 2 (T2)
================                 ================
BEGIN
  SELECT price FROM products     BEGIN
  WHERE id = 1                   SELECT price FROM products
  → price = 100                  WHERE id = 1
                                 → price = 100
                                 UPDATE products
                                 SET price = 150
                                 WHERE id = 1
                                 COMMIT
  SELECT price FROM products
  WHERE id = 1
  → price = 150  ← CHANGED!
  
COMMIT

Проблема в коде Java

public void nonRepeatableReadExample() throws SQLException {
    Connection conn = DriverManager.getConnection(url, user, pass);
    conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
    
    try {
        conn.setAutoCommit(false);
        
        // Первое чтение
        Statement stmt1 = conn.createStatement();
        ResultSet rs1 = stmt1.executeQuery("SELECT price FROM products WHERE id = 1");
        rs1.next();
        int price1 = rs1.getInt("price");  // price1 = 100
        System.out.println("First read: " + price1);
        
        // В этот момент другая транзакция может изменить строку
        Thread.sleep(2000);
        
        // Второе чтение
        Statement stmt2 = conn.createStatement();
        ResultSet rs2 = stmt2.executeQuery("SELECT price FROM products WHERE id = 1");
        rs2.next();
        int price2 = rs2.getInt("price");  // price2 = 150 (ИЗМЕНИЛОСЬ!)
        System.out.println("Second read: " + price2);
        
        if (price1 != price2) {
            System.out.println("NON-REPEATABLE READ!");
        }
        
        conn.commit();
    } catch (Exception e) {
        conn.rollback();
    }
}

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

Фантомное чтение — это когда результаты запроса изменяются между двумя запросами в рамках одной транзакции из-за добавления или удаления строк другой транзакцией.

Сценарий проблемы

Транзакция 1 (T1)                    Транзакция 2 (T2)
================                      ================
BEGIN
  SELECT COUNT(*) FROM accounts      BEGIN
  WHERE balance > 1000
  → count = 5                        INSERT INTO accounts
                                     (name, balance)
                                     VALUES ('New User', 1500)
                                     COMMIT
  SELECT COUNT(*) FROM accounts
  WHERE balance > 1000
  → count = 6  ← PHANTOM ROW!
  
COMMIT

Проблема в коде Java

public void phantomReadExample() throws SQLException {
    Connection conn = DriverManager.getConnection(url, user, pass);
    conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
    
    try {
        conn.setAutoCommit(false);
        
        // Первый запрос
        Statement stmt1 = conn.createStatement();
        ResultSet rs1 = stmt1.executeQuery(
            "SELECT id, name FROM accounts WHERE balance > 1000"
        );
        int count1 = 0;
        while (rs1.next()) {
            count1++;
            System.out.println("Account: " + rs1.getString("name"));
        }
        System.out.println("First count: " + count1);  // count1 = 5
        
        // В этот момент другая транзакция может добавить/удалить строки
        Thread.sleep(2000);
        
        // Второй запрос с теми же условиями
        Statement stmt2 = conn.createStatement();
        ResultSet rs2 = stmt2.executeQuery(
            "SELECT id, name FROM accounts WHERE balance > 1000"
        );
        int count2 = 0;
        while (rs2.next()) {
            count2++;
            System.out.println("Account: " + rs2.getString("name"));
        }
        System.out.println("Second count: " + count2);  // count2 = 6 (ИЗМЕНИЛОСЬ!)
        
        if (count1 != count2) {
            System.out.println("PHANTOM READ!");
        }
        
        conn.commit();
    } catch (Exception e) {
        conn.rollback();
    }
}

Ключевые различия

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

АспектНеповторяющееся чтениеФантомное чтение
Что изменяетсяСуществующая строка (UPDATE)Новые/удалённые строки (INSERT/DELETE)
Тип операцииUPDATEINSERT / DELETE
Количество результатовОстаётся прежнимИзменяется
Значения в строкахМеняютсяНовые/исчезают
ПримерЦена товара измениласьПоявился новый товар
РешениеREPEATABLE_READSERIALIZABLE

На уровне SQL

-- Неповторяющееся чтение: UPDATE
BEGIN;
SELECT price FROM products WHERE id = 1;  -- 100
-- Другая транзакция: UPDATE products SET price = 150 WHERE id = 1;
SELECT price FROM products WHERE id = 1;  -- 150 (CHANGED)
COMMIT;

-- Фантомное чтение: INSERT
BEGIN;
SELECT COUNT(*) FROM products WHERE price > 100;  -- 5
-- Другая транзакция: INSERT INTO products VALUES (...);
SELECT COUNT(*) FROM products WHERE price > 100;  -- 6 (NEW ROW)
COMMIT;

Матрица изоляции

УровеньDirty ReadNon-Repeatable ReadPhantom Read
READ_UNCOMMITTED
READ_COMMITTED
REPEATABLE_READ
SERIALIZABLE

(✓ = проблема отсутствует, ✗ = возможна проблема)

Практический пример: Spring/Hibernate

@Service
public class AccountService {
    
    @Transactional(
        isolation = Isolation.READ_COMMITTED
    )
    public void checkBalance() {
        // Может быть non-repeatable read
        Account account = accountRepository.findById(1L);
        System.out.println("Balance: " + account.getBalance());
        
        // ... другой код ...
        
        // Balance может измениться!
        account = accountRepository.findById(1L);
    }
    
    @Transactional(
        isolation = Isolation.REPEATABLE_READ
    )
    public void processAccounts() {
        // Не будет non-repeatable read
        // Но возможно phantom read
        List<Account> accounts = accountRepository.findByBalanceGreaterThan(1000);
        System.out.println("Count: " + accounts.size());
        
        // ... другой код ...
        
        // Количество может измениться!
        accounts = accountRepository.findByBalanceGreaterThan(1000);
    }
    
    @Transactional(
        isolation = Isolation.SERIALIZABLE
    )
    public void criticalOperation() {
        // Полная сериализуемость
        // Ни non-repeatable read, ни phantom read
        // Но медленнее!
    }
}

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

Выбор уровня изоляции

// 1. Большинство случаев — READ_COMMITTED (по умолчанию)
// Хороший баланс между производительностью и корректностью
@Transactional
public void normalOperation() { }

// 2. Если нужны последовательные чтения одних данных
// REPEATABLE_READ
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void consistentReads() { }

// 3. Только для критичных операций — SERIALIZABLE
// Снижает concurrent throughput
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalFinancialOperation() { }

Заключение

  • Неповторяющееся чтение: один и тот же РЯД имеет разные значения (UPDATE)
  • Фантомное чтение: результаты запроса меняются из-за новых/удалённых рядов (INSERT/DELETE)
  • READ_COMMITTED защищает от обоих
  • REPEATABLE_READ защищает от non-repeatable, но не от phantom
  • SERIALIZABLE защищает от всех проблем, но медленнее
  • Используй правильный уровень изоляции для твоего случая использования