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

Какие знаешь уровни изоляции транзакции в PostgreSQL?

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

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

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

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

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

Проблемы конкурентности при одновременном доступе к БД решаются разными уровнями изоляции. PostgreSQL поддерживает 4 стандартных уровня изоляции по ACID (Atomicity, Consistency, Isolation, Durability).

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

Перед разбором уровней понять проблемы:

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

  • Транзакция A читает данные, которые транзакция B еще не коммитила
  • Если B откатится, данные будут несогласованными

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

  • Транзакция A читает строку, потом B её изменяет и коммитит
  • Когда A читает еще раз, получает другое значение

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

  • Транзакция A выбирает набор строк по WHERE
  • Транзакция B добавляет новые строки, которые подходят под WHERE
  • Когда A делает SELECT еще раз, появляются новые строки

4. Serialization Anomaly

  • Результат выполнения нескольких параллельных транзакций отличается от последовательного выполнения

Уровень 1: READ UNCOMMITTED

Самый слабый уровень. Разрешает все проблемы конкурентности.

BEGIN ISOLATION LEVEL READ UNCOMMITTED;
-- Можно читать данные, которые еще не закоммичены (dirty read)
SELECT * FROM accounts WHERE id = 1;
COMMIT;

PostgreSQL особенность: На практике PostgreSQL не реализует полный READ UNCOMMITTED — используется READ COMMITTED вместо него. Это потому что PostgreSQL использует MVCC (Multi-Version Concurrency Control).

Проблемы:

  • Dirty read возможен
  • Non-repeatable read возможен
  • Phantom read возможен

Когда использовать: Очень редко. Только для аналитики где точность не критична.

Уровень 2: READ COMMITTED (По умолчанию)

Транзакция видит только коммиченные данные. Это уровень по умолчанию в PostgreSQL.

BEGIN ISOLATION LEVEL READ COMMITTED;

-- Пример: Перевод денег между счетами
SELECT balance FROM accounts WHERE id = 1;  -- Читаем текущий баланс
-- Если другая транзакция изменит баланс и закоммитит
-- Наша транзакция увидит новое значение при следующем SELECT
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

Проблемы:

  • Dirty read НЕ возможен (видим только коммиченные данные)
  • Non-repeatable read ВОЗМОЖЕН (другая транзакция может изменить данные между нашими SELECT'ами)
  • Phantom read ВОЗМОЖЕН

Пример non-repeatable read:

-- Транзакция A
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT balance FROM accounts WHERE id = 1; -- 1000
-- ... между тем транзакция B изменила и закоммитила
-- UPDATE accounts SET balance = 1500 WHERE id = 1;
SELECT balance FROM accounts WHERE id = 1; -- 1500 (другое значение!)
COMMIT;

Когда использовать: Большинство приложений. Хороший баланс между производительностью и консистентностью.

Уровень 3: REPEATABLE READ

Транзакция видит снимок данных на момент начала транзакции. Одна транзакция не видит изменения, сделанные другими транзакциями.

BEGIN ISOLATION LEVEL REPEATABLE READ;

SELECT balance FROM accounts WHERE id = 1;  -- 1000 (снимок на момент BEGIN)
-- Даже если другая транзакция изменит и закоммитит
SELECT balance FROM accounts WHERE id = 1;  -- 1000 (все еще видим старое значение)
COMMIT;

Проблемы:

  • Dirty read НЕ возможен
  • Non-repeatable read НЕ возможен (видим снимок, сделанный в начале транзакции)
  • Phantom read ВОЗМОЖЕН в PostgreSQL (в других БД может быть иначе)

Пример phantom read:

-- Транзакция A
BEGIN ISOLATION LEVEL REPEATABLE READ;

SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- 5 заказов

-- Транзакция B: INSERT INTO orders ... WHERE status = 'pending';
-- Транзакция B коммитит

SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- Может быть 6!
-- (Новые строки видны, хотя существующие не изменились)

COMMIT;

Когда использовать: Когда нужна стабильность чтения, но phantom read не критичен.

Уровень 4: SERIALIZABLE

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

BEGIN ISOLATION LEVEL SERIALIZABLE;

SELECT balance FROM accounts WHERE id = 1; -- 1000
-- Если другая транзакция пытается изменить эту строку
-- она будет заблокирована или откачена
SELECT balance FROM accounts WHERE id = 1; -- 1000 (гарантированно)
COMMIT;

Особенность PostgreSQL: Использует SSI (Serializable Snapshot Isolation), а не полные блокировки. Это означает, что транзакции не блокируют друг друга, но если обнаруживается конфликт (serialization anomaly), одна из них откатывается с ошибкой serialization failure.

// Node.js пример
const client = await pool.connect();

try {
  await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
  
  const result1 = await client.query(
    'SELECT balance FROM accounts WHERE id = 1 FOR UPDATE'
  );
  
  if (result1.rows[0].balance >= 100) {
    await client.query(
      'UPDATE accounts SET balance = balance - 100 WHERE id = 1'
    );
  }
  
  await client.query('COMMIT');
} catch (error) {
  if (error.code === '40001') { // Serialization failure
    // Повтори транзакцию
    console.log('Serialization conflict, retrying...');
  }
  await client.query('ROLLBACK');
}

Проблемы:

  • Dirty read НЕ возможен
  • Non-repeatable read НЕ возможен
  • Phantom read НЕ возможен
  • Serialization anomaly НЕ возможно

Когда использовать: Критичные финансовые операции, где нужна полная консистентность. Но будьте готовы обрабатывать serialization failure ошибки.

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

УровеньDirty ReadNon-Rep ReadPhantom ReadAnomaly
READ UNCOMMITTEDДаДаДаДа
READ COMMITTEDНетДаДаДа
REPEATABLE READНетНетДа*Да*
SERIALIZABLEНетНетНетНет

*В PostgreSQL REPEATABLE READ может позволить phantom read и anomaly из-за SSI

Практические примеры

Пример 1: Перевод денег (READ COMMITTED)

async function transferMoney(
  fromId: number,
  toId: number,
  amount: number,
) {
  const client = await pool.connect();
  
  try {
    await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
    
    // Проверяем баланс
    const from = await client.query(
      'SELECT balance FROM accounts WHERE id = $1 FOR UPDATE',
      [fromId],
    );
    
    if (from.rows[0].balance < amount) {
      throw new Error('Insufficient funds');
    }
    
    // Снимаем деньги
    await client.query(
      'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
      [amount, fromId],
    );
    
    // Пополняем счет
    await client.query(
      'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
      [amount, toId],
    );
    
    await client.query('COMMIT');
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
}

Пример 2: Критичная операция (SERIALIZABLE)

async function deductQuota(userId: number, amount: number) {
  const client = await pool.connect();
  const maxRetries = 3;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      await client.query('BEGIN ISOLATION LEVEL SERIALIZABLE');
      
      const quota = await client.query(
        'SELECT quota FROM users WHERE id = $1 FOR UPDATE',
        [userId],
      );
      
      if (quota.rows[0].quota < amount) {
        throw new Error('Insufficient quota');
      }
      
      await client.query(
        'UPDATE users SET quota = quota - $1 WHERE id = $2',
        [amount, userId],
      );
      
      await client.query('COMMIT');
      return;
    } catch (error) {
      await client.query('ROLLBACK');
      
      if (error.code === '40001' && attempt < maxRetries - 1) {
        // Serialization failure, retry
        await new Promise(r => setTimeout(r, Math.random() * 100));
        continue;
      }
      
      throw error;
    }
  }
}

Механизмы PostgreSQL

MVCC (Multi-Version Concurrency Control)

  • PostgreSQL хранит разные версии строк
  • Каждая транзакция видит снимок данных
  • Нет блокировок на чтение

FOR UPDATE / FOR SHARE

-- Блокируем строку для исключительного доступа
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;

-- Блокируем строку для общего (shared) доступа
SELECT * FROM accounts WHERE id = 1 FOR SHARE;

Best Practices

  1. По умолчанию READ COMMITTED — это хороший выбор для большинства приложений
  2. Используй FOR UPDATE для синхронизации изменений
  3. SERIALIZABLE для критичных операций — но обрабатывай ошибки
  4. Минимизируй время транзакции — особенно для SERIALIZABLE
  5. Тестируй конкурентность — race conditions сложно находить

Заключение

Выбор уровня изоляции — это баланс между:

  • Консистентностью (безопасность данных)
  • Производительностью (пропускная способность)
  • Сложностью (обработка ошибок)

Для большинства приложений READ COMMITTED с правильным использованием блокировок (FOR UPDATE) — оптимальный выбор.