Какие знаешь уровни изоляции транзакции в PostgreSQL?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Уровни изоляции транзакций в 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 Read | Non-Rep Read | Phantom Read | Anomaly |
|---|---|---|---|---|
| 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
- По умолчанию READ COMMITTED — это хороший выбор для большинства приложений
- Используй FOR UPDATE для синхронизации изменений
- SERIALIZABLE для критичных операций — но обрабатывай ошибки
- Минимизируй время транзакции — особенно для SERIALIZABLE
- Тестируй конкурентность — race conditions сложно находить
Заключение
Выбор уровня изоляции — это баланс между:
- Консистентностью (безопасность данных)
- Производительностью (пропускная способность)
- Сложностью (обработка ошибок)
Для большинства приложений READ COMMITTED с правильным использованием блокировок (FOR UPDATE) — оптимальный выбор.