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

Какие должны быть свойства у транзакции?

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

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

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

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

Какие должны быть свойства у транзакции:

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

  1. Всегда используй транзакции для multi-step операций
  2. Выбери нужный уровень изоляции (обычно REPEATABLE READ)
  3. Не держи открытые транзакции долго — это блокирует ресурсы
  4. Добавь retry logic для deadlock'ов
  5. Используй connection pooling (pg.Pool)
  6. Логируй транзакции — помогает отладке
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 = гарантия что твои данные целы и валидны.

Какие должны быть свойства у транзакции? | PrepBro