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

Когда используешь транзакции?

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

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

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

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

# Когда используешь транзакции?

Транзакции - это критически важный инструмент для обеспечения консистентности данных. Расскажу, когда и как их правильно использовать.

Определение и ACID

Транзакция - это последовательность операций с БД, которые либо все выполняются успешно, либо все откатываются. Это гарантируется ACID принципами:

  • A (Atomicity) - все или ничего
  • C (Consistency) - данные остаются консистентными
  • I (Isolation) - транзакции изолированы друг от друга
  • D (Durability) - после коммита, данные сохранены

Когда ОБЯЗАТЕЛЬНО нужны транзакции?

1. Переводы денег между счетами

Это класический пример - операция должна быть atomicy:

DB::beginTransaction();
try {
    // Снять со счета отправителя
    Account::where('id', $fromAccountId)->decrement('balance', $amount);
    
    // Положить на счет получателя
    Account::where('id', $toAccountId)->increment('balance', $amount);
    
    // Записать транзакцию
    Transaction::create([
        'from_account_id' => $fromAccountId,
        'to_account_id' => $toAccountId,
        'amount' => $amount,
        'status' => 'completed'
    ]);
    
    DB::commit();
} catch (Exception $e) {
    DB::rollBack();
    throw new TransactionFailedException('Transfer failed');
}

Почему транзакция критична: если произойдет ошибка между снятием и положением денег, то деньги исчезнут из системы.

2. Создание заказа с товарами

Заказ + несколько товаров должны создаться вместе:

DB::transaction(function () use ($userId, $items) {
    // Создание заказа
    $order = Order::create([
        'user_id' => $userId,
        'total_price' => $totalPrice,
        'status' => 'pending'
    ]);
    
    // Добавление товаров в заказ
    foreach ($items as $item) {
        OrderItem::create([
            'order_id' => $order->id,
            'product_id' => $item['product_id'],
            'quantity' => $item['quantity'],
            'price' => $item['price']
        ]);
        
        // Уменьшение количества на складе
        Product::where('id', $item['product_id'])
            ->decrement('stock', $item['quantity']);
    }
    
    // Логирование
    OrderLog::create([
        'order_id' => $order->id,
        'action' => 'created',
        'details' => json_encode($items)
    ]);
    
    return $order;
});

Почему: если создадим заказ, но не создадим товары - заказ будет пустой. Если товары создадим, но заказ упадет - товары повиснут в таблице.

3. Обновление материализованного представления

Если у нас есть кэш данных, который нужно обновить атомарно:

DB::transaction(function () use ($userId) {
    // Пересчитываем статистику
    $stats = User::find($userId)->getStats();
    
    // Обновляем кэш
    UserStats::updateOrCreate(
        ['user_id' => $userId],
        [
            'total_orders' => $stats['orders'],
            'total_spent' => $stats['spent'],
            'last_updated' => now()
        ]
    );
    
    // Обновляем основные данные
    User::where('id', $userId)->update([
        'stats_updated_at' => now()
    ]);
});

4. Многошаговые операции с проверками

DB::transaction(function () use ($productId, $quantity) {
    // Проверяем наличие товара
    $product = Product::where('id', $productId)->lockForUpdate()->first();
    
    if ($product->stock < $quantity) {
        throw new OutOfStockException('Not enough items');
    }
    
    // Уменьшаем остаток
    $product->decrement('stock', $quantity);
    
    // Создаем лог
    ProductLog::create([
        'product_id' => $productId,
        'action' => 'reserved',
        'quantity' => $quantity
    ]);
});

Когда транзакции НЕ критичны?

1. Простые read-only операции

// Транзакция не нужна для чтения
$user = User::find($id);
$orders = $user->orders()->get();

// Это окей и без транзакции
$count = User::count();

2. Независимые операции

// Эти операции независимы, не нужна транзакция
User::where('inactive_days', '>', 365)->delete();
Session::whereNull('user_id')->delete();
Log::where('created_at', '<', now()->subMonths(3))->delete();

3. Логирование некритичных событий

// Логирование можно делать без транзакции
// (но желательно асинхронно через очередь)
ActivityLog::create([
    'user_id' => auth()->id(),
    'action' => 'viewed_page',
    'url' => request()->url()
]);

Изоляция в транзакциях

В PostgreSQL есть разные уровни изоляции, которые влияют на поведение:

READ UNCOMMITTED (наименее защищено)

// Можно читать незавершенные изменения
DB::transaction(function () {
    User::find($id)->update(['balance' => 100]);
    // Другая транзакция может прочитать 100 ДО коммита
}, null, \Illuminate\Database\Connection::TRANSACTION_READ_UNCOMMITTED);

READ COMMITTED (по умолчанию)

// Читаем только завершенные изменения
DB::transaction(function () {
    User::find($id)->update(['balance' => 100]);
    // Другая транзакция прочитает старое значение ДО коммита
});

REPEATABLE READ

// Один скачок данных на всю транзакцию
DB::transaction(function () {
    $balance1 = User::find($id)->balance;
    // ... какой-то код ...
    $balance2 = User::find($id)->balance; // Гарантированно == $balance1
}, null, \Illuminate\Database\Connection::TRANSACTION_REPEATABLE_READ);

SERIALIZABLE (наиболее защищено)

// Максимум безопасности, минус скорость
DB::transaction(function () {
    // Гарантия полной изоляции от других транзакций
}, null, \Illuminate\Database\Connection::TRANSACTION_SERIALIZABLE);

Лучшие практики

1. Держи транзакции короткими

// ❌ Плохо - долгая транзакция
DB::transaction(function () {
    // ... создание заказа ...
    sleep(5); // Ждем ответа от внешнего API
    // ... обновление ...
});

// ✅ Хорошо - транзакция только для БД
DB::transaction(function () {
    // ... создание заказа ...
});

// Асинхронная операция отдельно
Dispatch(new ProcessOrderAsync($orderId));

2. Используй lockForUpdate() для критичных операций

// Блокируем строку на чтение и изменение
$account = Account::where('id', $fromId)
    ->lockForUpdate()
    ->first();

if ($account->balance >= $amount) {
    $account->decrement('balance', $amount);
}

3. Обрабатывай исключения правильно

try {
    DB::transaction(function () {
        // ... логика ...
    });
} catch (PDOException $e) {
    // Логируем для анализа
    Log::error('Transaction failed', ['error' => $e->getMessage()]);
    // Пробрасываем дальше
    throw new DatabaseException('Failed to process transaction');
}

4. Используй savepoint для вложенных операций

DB::transaction(function () {
    // Основная операция
    Order::create(['total' => 100]);
    
    DB::transaction(function () {
        // Вложенная операция
        OrderLog::create(['message' => 'created']);
        // Если упадет - откатится только эта часть
    });
});

Итог

Используй транзакции когда:

  • Несколько операций должны быть атомичными
  • Данные должны остаться консистентными при ошибке
  • Работаешь с деньгами, заказами или другими критичными данными

Не используй когда:

  • Читаешь данные
  • Операции независимы
  • Можешь логировать/обновлять асинхронно

Транзакции - это не щит от всех проблем, это инструмент для гарантии консистентности. Используй правильно!

Когда используешь транзакции? | PrepBro