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

На каком уровне изоляции отсутствует фантомное чтение в PostgreSQL

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

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

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

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

Фантомное чтение в PostgreSQL и уровни изоляции

Этот вопрос требует глубокого понимания транзакций и уровней изоляции. В PostgreSQL фантомное чтение отсутствует на уровне SERIALIZABLE. На уровне REPEATABLE READ (которая в PostgreSQL реализует Snapshot Isolation) фантомное чтение ТАКЖЕ отсутствует.

Что такое фантомное чтение?

Фантомное чтение (Phantom Read) — это ситуация, когда одна транзакция дважды выполняет один и тот же запрос SELECT и получает разное количество строк.

// Пример фантомного чтения:
// Транзакция 1:
SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- Результат: 5

// Между тем Транзакция 2 добавляет новый заказ:
INSERT INTO orders (status) VALUES ('pending');
COMMIT;

// Транзакция 1 снова выполняет тот же запрос:
SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- Результат: 6 (фантом!)

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

PostgreSQL поддерживает 3 уровня изоляции (хотя 4 названия в SQL стандарте):

1. READ UNCOMMITTED

Connection conn = dataSource.getConnection();
conn.setTransactionIsolation(
    Connection.TRANSACTION_READ_UNCOMMITTED
);
// В PostgreSQL это то же самое, что READ COMMITTED
// Уязвима для: Dirty Read, Non-Repeatable Read, Phantom Read
-- В SQL:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

2. READ COMMITTED (по умолчанию)

Connection conn = dataSource.getConnection();
conn.setTransactionIsolation(
    Connection.TRANSACTION_READ_COMMITTED
);
// Исключает: Dirty Read
// Уязвима для: Non-Repeatable Read, Phantom Read
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- Пример фантомного чтения при READ COMMITTED:
BEGIN ISOLATION LEVEL READ COMMITTED;

-- Запрос 1: Найти все активные заказы
SELECT * FROM orders WHERE status = 'active';
-- Результат: 3 заказа (id: 1, 2, 3)

-- Одновременно другой процесс добавляет заказ:
INSERT INTO orders (id, status) VALUES (4, 'active');
COMMIT;

-- Запрос 2: Выполнить тот же SELECT
SELECT * FROM orders WHERE status = 'active';
-- Результат: 4 заказа (id: 1, 2, 3, 4) -- ФАНТОМ!

COMMIT;

3. REPEATABLE READ

Connection conn = dataSource.getConnection();
conn.setTransactionIsolation(
    Connection.TRANSACTION_REPEATABLE_READ
);
// Исключает: Dirty Read, Non-Repeatable Read, Phantom Read
// PostgreSQL реализует это через Snapshot Isolation
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- В PostgreSQL на REPEATABLE READ нет фантомного чтения!
BEGIN ISOLATION LEVEL REPEATABLE READ;

-- Создаётся снимок данных ДА НАЧАЛО ТРАНЗАКЦИИ
SELECT * FROM orders WHERE status = 'active';
-- Результат: 3 заказа

-- Другой процесс добавляет и коммитит:
INSERT INTO orders (status) VALUES ('active');
COMMIT;

-- Запрос повторяется:
SELECT * FROM orders WHERE status = 'active';
-- Результат: всё ещё 3 заказа (видим снимок от начала транзакции)
-- НЕТ фантомного чтения!

COMMIT;

4. SERIALIZABLE

Connection conn = dataSource.getConnection();
conn.setTransactionIsolation(
    Connection.TRANSACTION_SERIALIZABLE
);
// Исключает: ВСЕ аномалии (Dirty Read, Non-Repeatable Read, Phantom Read)
// Самый строгий уровень
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Трансакции выполняются как будто последовательно
-- Фантомного чтения ТОЧНО нет
BEGIN ISOLATION LEVEL SERIALIZABLE;

SELECT * FROM orders WHERE status = 'active';
-- Результат: 3 заказа

COMMIT; -- Если другой процесс уже коммитил INSERT, 
         -- эта транзакция откатится!

Таблица аномалий по уровням

Уровень изоляции       | Dirty Read | Non-Rep Read | Phantom Read
---------------------------------------------------------------------------
READ UNCOMMITTED        |    ДА      |      ДА      |      ДА
READ COMMITTED          |    НЕТ     |      ДА      |      ДА
REPEATABLE READ         |    НЕТ     |      НЕТ     |      НЕТ (в PG!)
SERIALIZABLE           |    НЕТ     |      НЕТ     |      НЕТ

PostgreSQL особенность: Snapshot Isolation

PostgreSQL реализует REPEATABLE READ используя Snapshot Isolation (SI), а не двумерные блокировки. Это означает:

import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

public class OrderService {
    
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public List<Order> getActiveOrders() {
        // В начале транзакции создаётся snapshot всех данных
        // Все последующие SELECT видят этот snapshot
        // Изменения других транзакций НЕ видны
        
        // Поэтому:  ФАНТОМНОГО ЧТЕНИЯ НЕТ
        return orderRepository.findByStatus("active");
    }
}

Пример в JDBC

import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;

public class IsolationLevelExample {
    
    public static void demonstrateRepeatableReadNoPhantom() {
        try (Connection conn = getConnection()) {
            // Установить REPEATABLE READ
            conn.setTransactionIsolation(
                Connection.TRANSACTION_REPEATABLE_READ
            );
            conn.setAutoCommit(false);
            
            // Первый SELECT
            try (Statement stmt = conn.createStatement()) {
                ResultSet rs = stmt.executeQuery(
                    "SELECT COUNT(*) FROM orders WHERE status = 'active'"
                );
                rs.next();
                int count1 = rs.getInt(1);
                System.out.println("First count: " + count1);
            }
            
            // Ждём, пока другой процесс добавит строку
            Thread.sleep(2000);
            
            // Второй SELECT на том же уровне
            try (Statement stmt = conn.createStatement()) {
                ResultSet rs = stmt.executeQuery(
                    "SELECT COUNT(*) FROM orders WHERE status = 'active'"
                );
                rs.next();
                int count2 = rs.getInt(1);
                System.out.println("Second count: " + count2);
                
                // В PostgreSQL: count1 == count2 (нет фантома)
            }
            
            conn.commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Spring Data JPA с правильным уровнем

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

public interface OrderRepository extends JpaRepository<Order, Long> {
    
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    @Query("SELECT o FROM Order o WHERE o.status = :status")
    List<Order> findByStatus(String status);
    
    @Transactional(isolation = Isolation.SERIALIZABLE)
    @Query("SELECT o FROM Order o FOR UPDATE")
    List<Order> findAllForUpdate();
}

Практический сценарий

public class OrderProcessing {
    
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void processOrders() {
        // Инициализируется snapshot
        List<Order> orders = orderRepository.findByStatus("pending");
        
        if (orders.isEmpty()) {
            return;
        }
        
        // Даже если другой процесс добавит заказы со статусом 'pending',
        // мы их не увидим (Snapshot Isolation)
        for (Order order : orders) {
            processOrder(order);
        }
        
        // Проверка в конце транзакции
        List<Order> recheck = orderRepository.findByStatus("pending");
        // recheck.size() == orders.size() (гарантированно в PG)
    }
}

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

public class FinancialTransaction {
    
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void transferMoney(String fromAccount, String toAccount, 
                              BigDecimal amount) {
        // Требуется максимальная изоляция
        Account from = accountRepository.findById(fromAccount).orElseThrow();
        Account to = accountRepository.findById(toAccount).orElseThrow();
        
        if (from.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException();
        }
        
        from.withdraw(amount);
        to.deposit(amount);
        
        accountRepository.save(from);
        accountRepository.save(to);
        
        // Если другая транзакция читала эти счета, может быть откат
    }
}

Конфигурация в Spring Boot

# application.properties
spring.datasource.hikari.isolation-level=READ_COMMITTED

# Или программно:
spring.jpa.properties.hibernate.jdbc.batch_size=20
@Configuration
public class DataSourceConfig {
    
    @Bean
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setIsolationLevel(
            Connection.TRANSACTION_REPEATABLE_READ
        );
        return dataSource;
    }
}

Мониторинг транзакций

-- Посмотреть текущие транзакции
SELECT * FROM pg_stat_activity 
WHERE state = 'active';

-- Проверить уровень изоляции
SHOW transaction_isolation;

-- Посмотреть блокировки
SELECT * FROM pg_locks WHERE NOT granted;

Заключение

Правильный ответ:

В PostgreSQL фантомное чтение отсутствует на:

  1. REPEATABLE READ — благодаря Snapshot Isolation (SI)

    • Это реальное преимущество PostgreSQL
    • Не требует блокировок для чтения
    • Высокая параллельность
  2. SERIALIZABLE — наиболее строгий уровень

    • Использует SSI (Serializable Snapshot Isolation)
    • Может откатить транзакции в конфликте
    • Редко используется

В Java разработке:

  • По умолчанию используется READ_COMMITTED
  • Для критичных операций: REPEATABLE_READ
  • Для финансовых операций: SERIALIZABLE

Это важная компетенция при работе с базами данных в production системах.