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

Какие знаешь уровни изоляции транзакций, отсутствующие в PostgreSQL?

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

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

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

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

Уровни изоляции транзакций, отсутствующие в PostgreSQL

В SQL стандарте определено 4 уровня изоляции транзакций, но PostgreSQL реализует их по-другому. Более того, PostgreSQL отсутствуют некоторые уровни, а другие имеют нестандартное поведение.

Стандартные уровни по ANSI SQL

ANSI SQL уровни:
1. READ UNCOMMITTED
2. READ COMMITTED
3. REPEATABLE READ
4. SERIALIZABLE

PostgreSQL реализует:**

PostgreSQL уровни:
1. READ UNCOMMITTED (с заметками)
2. READ COMMITTED (по умолчанию)
3. REPEATABLE READ
4. SERIALIZABLE

Проблема 1: READ UNCOMMITTED в PostgreSQL

В ANSI SQL: READ UNCOMMITTED позволяет читать неподтвёрженные данные (Dirty Read).

В PostgreSQL: READ UNCOMMITTED не реализован!

-- PostgreSQL игнорирует READ UNCOMMITTED и обрабатывает как READ COMMITTED
BEGIN TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- Это эквивалентно:
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- Dirty Read невозможен в PostgreSQL
TRANSACTION 1: UPDATE users SET balance = 100;
TRANSACTION 2: BEGIN ISOLATION LEVEL READ UNCOMMITTED;
TRANSACTION 2: SELECT balance FROM users; -- 100 (но это READ COMMITTED, не READ UNCOMMITTED!)
TRANSACTION 1: ROLLBACK;
TRANSACTION 2: SELECT balance FROM users; -- Исходное значение (Dirty Read не произойдёт)

Почему PostgreSQL так сделала:

  • Слабый контроль версий (MVCC) предотвращает Dirty Read даже при LOW изоляции
  • Это безопаснее и быстрее
  • Разработчик не может получить Dirty Read в PostgreSQL

Java примечание:

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void someMethod() {
    // В PostgreSQL это будет READ COMMITTED!
    // Фактическое поведение отличается от ожидаемого
}

Проблема 2: Phanton Read в PostgreSQL REPEATABLE READ

ANSI SQL REPEATABLE READ: Может происходить Phantom Read (появление новых строк).

PostgreSQL REPEATABLE READ: Phantom Read НЕ происходит!

-- ANSI SQL REPEATABLE READ поведение
TRANSACTION 1: SELECT COUNT(*) FROM orders WHERE status = "pending";
-- Результат: 5 строк

TRANSACTION 2: INSERT INTO orders (status) VALUES ("pending"), ("pending");
COMMIT;

TRANSACTION 1: SELECT COUNT(*) FROM orders WHERE status = "pending";
-- ANSI: может быть 7 (PHANTOM READ)
-- PostgreSQL: все ещё 5 (снимок зафиксирован в начале транзакции)

TRANSACTION 1: COMMIT;

Причина:

  • PostgreSQL использует Snapshot Isolation вместо классического MVCC
  • Каждая транзакция видит консистентный снимок данных на момент начала
  • Гораздо строже, чем ANSI REPEATABLE READ
@Transactional(isolation = Isolation.REPEATABLE_READ)
public List<Order> getPendingOrders() {
    // В PostgreSQL это эффективнее ANSI REPEATABLE READ
    // Но отличается поведением
    return orderRepository.findByStatus("pending");
}

Проблема 3: SERIALIZABLE в PostgreSQL

PostgreSQL SERIALIZABLE реализован через Serializable Snapshot Isolation (SSI).

Отличие от ANSI SERIALIZABLE:

-- ANSI SERIALIZABLE: как будто транзакции выполняются последовательно
-- PostgreSQL SERIALIZABLE: более оптимистичный подход

-- Пример конфликта Serialization
TRANSACTION 1:
  SELECT SUM(amount) FROM accounts;  -- 1000
  INSERT INTO ledger VALUES(100);    -- Добавляем транзакцию

TRANSACTION 2:
  SELECT SUM(amount) FROM accounts;  -- 1000
  INSERT INTO ledger VALUES(200);    -- Добавляем транзакцию

-- ANSI SERIALIZABLE: одна из транзакций отменяется
-- PostgreSQL SERIALIZABLE (SSI): также отменяется, но через другой механизм

Проблема: false negatives в SSI

-- PostgreSQL может не обнаружить некоторые конфликты
TRANSACTION 1:
  SELECT * FROM users WHERE age > 18;    -- Возвращает пустой набор
  INSERT INTO users VALUES(25, "Alice"); -- Добавляем 25-летнюю

TRANSACTION 2:
  SELECT * FROM users WHERE age > 18;    -- Возвращает пустой набор
  INSERT INTO users VALUES(30, "Bob");   -- Добавляем 30-летнего

Both COMMIT;  -- PostgreSQL может разрешить, но это нарушает Serializable!

Сравнение реализаций

УровеньANSI SQLPostgreSQLРазличие
READ UNCOMMITTEDDirty Read возможенРаботает как READ COMMITTEDPostgreSQL безопаснее
READ COMMITTEDNon-Repeatable ReadNon-Repeatable ReadИдентично
REPEATABLE READPhantom Read возможенPhantom Read невозможенPostgreSQL строже (Snapshot Isolation)
SERIALIZABLEПолная серializabilitySSI (оптимистичный подход)Разные механизмы

Практические последствия для Java разработчика

Проблема 1: Ожидаемое поведение != Фактическое

// Разработчик пишет код с READ UNCOMMITTED ожиданиями
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void processOrder(Long orderId) {
    Order order = orderRepository.findById(orderId); // В PostgreSQL это READ COMMITTED!
    if (order.getStatus().equals("PENDING")) {
        // Может быть non-repeatable read здесь
    }
}

// Если потом эта же функция переместится на MySQL, поведение изменится!

Проблема 2: Serialization Errors в PostgreSQL SERIALIZABLE

@Transactional(isolation = Isolation.SERIALIZABLE)
public void transferMoney(Long from, Long to, BigDecimal amount) {
    // В PostgreSQL может быть SerializationFailureException
    Account fromAccount = accountRepository.findById(from);
    Account toAccount = accountRepository.findById(to);
    
    // PostgreSQL SSI может отменить эту транзакцию
    if (fromAccount.getBalance().compareTo(amount) >= 0) {
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        toAccount.setBalance(toAccount.getBalance().add(amount));
    }
    // Нужно обработать SerializationFailureException и retry
}

// Правильно с retry logic
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transferMoneyWithRetry(Long from, Long to, BigDecimal amount) {
    for (int attempt = 0; attempt < 3; attempt++) {
        try {
            Account fromAccount = accountRepository.findById(from);
            Account toAccount = accountRepository.findById(to);
            
            if (fromAccount.getBalance().compareTo(amount) >= 0) {
                fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
                toAccount.setBalance(toAccount.getBalance().add(amount));
            }
            return; // Успешно
        } catch (SerializationFailureException e) {
            if (attempt == 2) throw e;
            // Retry
        }
    }
}

PostgreSQL предлагает альтернативы

1. SELECT FOR UPDATE вместо REPEATABLE READ

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT a FROM Account a WHERE a.id = ?1")
    Account findByIdForUpdate(Long id);
}

@Transactional
public void transferMoney(Long from, Long to, BigDecimal amount) {
    // SELECT FOR UPDATE предотвращает конфликты
    Account fromAccount = accountRepository.findByIdForUpdate(from);
    Account toAccount = accountRepository.findByIdForUpdate(to);
    
    // Безопасно даже при READ COMMITTED
    if (fromAccount.getBalance().compareTo(amount) >= 0) {
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        toAccount.setBalance(toAccount.getBalance().add(amount));
    }
}

2. DEFERRABLE транзакции для hot standby

BEGIN TRANSACTION
  ISOLATION LEVEL SERIALIZABLE
  READ ONLY
  DEFERRABLE;  -- Может быть отложена до безопасного момента на standby

SELECT * FROM users WHERE id = 1;
COMMIT;

Матрица поддержки Phantom Read

БДREAD UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE
MySQLДаНетНетНет
PostgreSQLКак RCНетНЕТ (Snapshot)Нет (SSI)
OracleНет (N/A)НетДаНет (Snapshot)
SQL ServerНетНетДаНет

Рекомендации для Java разработчиков

Если используешь PostgreSQL:

// 1. Не полагайся на READ UNCOMMITTED
@Transactional // READ COMMITTED по умолчанию в PostgreSQL
public void readData() { /* ... */ }

// 2. REPEATABLE READ в PostgreSQL безопаснее чем в других БД
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void analyticsQuery() { /* ... */ }

// 3. SERIALIZABLE требует retry logic
@Transactional(isolation = Isolation.SERIALIZABLE)
public void criticalOperation() {
    // Обработай SerializationFailureException
}

// 4. Предпочитай SELECT FOR UPDATE для критичных операций
@Transactional
public void moneytransfer() {
    findByIdForUpdate(id); // Pessimistic lock
}

Заключение

PostgreSQL отличается от ANSI SQL в реализации изоляции:

  1. READ UNCOMMITTED не существует — работает как READ COMMITTED
  2. REPEATABLE READ строже — Snapshot Isolation, нет Phantom Read
  3. SERIALIZABLE другой — SSI вместо полной сериализации

Практическое значение:

  • Код, который работает на PostgreSQL, может не работать на MySQL/Oracle
  • PostgreSQL более безопасна по умолчанию
  • Нужно тестировать многопроцессные сценарии на целевой БД
  • Документируй требования к изоляции явно

Это ещё одна причина избегать привязки к конкретной БД и тестировать с целевым движком.

Какие знаешь уровни изоляции транзакций, отсутствующие в PostgreSQL? | PrepBro