Какие должны быть свойства у транзакции?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Какие должны быть свойства у транзакции:
ACID-свойства
Транзакция в БД должна иметь четыре критических свойства:
1. Atomicity (Атомарность)
Определение: Транзакция либо полностью выполнится, либо полностью откатится. Не бывает "половинного" результата.
Пример проблемы без атомарности:
Преводим 100 рублей со счета A на счет B:
- Вычли 100 с A
- Упал сервер
- Добавили 100 на B — НИКОГДА не произойдет
Результат: потеря 100 рублей!
С атомарной транзакцией:
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('UPDATE accounts SET balance = balance - 100 WHERE id = $1', [accountA]);
await client.query('UPDATE accounts SET balance = balance + 100 WHERE id = $1', [accountB]);
await client.query('COMMIT');
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
Или с ORMом (TypeORM, Sequelize):
await queryRunner.startTransaction();
try {
await queryRunner.manager.update(Account, accountA, { balance: () => 'balance - 100' });
await queryRunner.manager.update(Account, accountB, { balance: () => 'balance + 100' });
await queryRunner.commitTransaction();
} catch (e) {
await queryRunner.rollbackTransaction();
throw e;
}
2. Consistency (Согласованность)
Определение: БД переходит из одного согласованного состояния в другое. Нарушения constraints не допускаются.
Пример: Если у нас есть constraint, что balance не может быть отрицательным:
ALTER TABLE accounts ADD CONSTRAINT check_balance CHECK (balance >= 0);
БД не позволит выполнить транзакцию, которая нарушит это правило.
В приложении это означает:
- Все бизнес-правила должны быть валидированы ДО попадания в БД
- Или через DB constraints (CHECK, FOREIGN KEY)
- Или через application logic
if (accountA.balance < 100) {
throw new InsufficientFundsError();
}
// Теперь безопасно переводить
3. Isolation (Изоляция)
Определение: Параллельные транзакции не мешают друг другу. Одна не видит "грязные" данные другой.
Проблема без изоляции: Транзакция 1 читает баланс A = 100. Транзакция 2 вычитает 50 и коммитит. Транзакция 1 думает, что баланс еще 100 (но на самом деле 50). Результат: "грязное" чтение (dirty read).
Уровни изоляции (от слабой к сильной):
- READ UNCOMMITTED (опасно): видит незакоммиченные данные
- READ COMMITTED (стандарт): видит только закоммиченные данные
- REPEATABLE READ (MySQL default): гарантирует что данные не меняются внутри транзакции
- SERIALIZABLE (самый сильный): как будто транзакции выполняются по очереди
Выбор уровня в Node.js:
await client.query('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
await client.query('BEGIN');
// ... запросы
await client.query('COMMIT');
Проблемы при слабой изоляции:
- Dirty reads: видим незакоммиченные данные
- Non-repeatable reads: одно поле меняется внутри транзакции
- Phantom reads: новые строки появляются при повторном SELECT
4. Durability (Долговечность)
Определение: После коммита данные сохраняются навсегда, даже при краше БД.
Как это работает:
- БД пишет данные в Write-Ahead Log (WAL)
- После успешного COMMIT, данные гарантированно на диске
- При краше БД восстанавливается из WAL
Пример проблемы: БД коммитит успешно, но приложение не получило подтверждение из-за сетевой ошибки. Приложение повторяет операцию. Дублирование!
Решение — идемпотентность:
// Используем уникальный idempotency_key
const existingTransaction = await db.query(
'SELECT * FROM transactions WHERE idempotency_key = $1',
[idempotencyKey]
);
if (existingTransaction.rows.length > 0) {
return existingTransaction.rows[0]; // Уже обработано
}
await db.query('BEGIN');
try {
const result = await db.query(
'INSERT INTO transactions (idempotency_key, amount) VALUES ($1, $2) RETURNING *',
[idempotencyKey, amount]
);
await db.query('COMMIT');
return result.rows[0];
} catch (e) {
await db.query('ROLLBACK');
throw e;
}
Практические рекомендации для Node.js
- Всегда используй транзакции для multi-step операций
- Выбери нужный уровень изоляции (обычно REPEATABLE READ)
- Не держи открытые транзакции долго — это блокирует ресурсы
- Добавь retry logic для deadlock'ов
- Используй connection pooling (pg.Pool)
- Логируй транзакции — помогает отладке
const pool = new Pool();
const maxRetries = 3;
for (let attempt = 0; attempt < maxRetries; attempt++) {
const client = await pool.connect();
try {
await client.query('BEGIN');
// ... твои запросы
await client.query('COMMIT');
break; // Успех
} catch (e) {
await client.query('ROLLBACK');
if (e.code === '40P01' && attempt < maxRetries - 1) {
// Deadlock — пересчитаем
await new Promise(r => setTimeout(r, 100 * (attempt + 1)));
continue;
}
throw e;
} finally {
client.release();
}
}
ATOMICITY + CONSISTENCY + ISOLATION + DURABILITY = гарантия что твои данные целы и валидны.