Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Когда используешь транзакции?
Транзакции - это критически важный инструмент для обеспечения консистентности данных. Расскажу, когда и как их правильно использовать.
Определение и 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']);
// Если упадет - откатится только эта часть
});
});
Итог
Используй транзакции когда:
- Несколько операций должны быть атомичными
- Данные должны остаться консистентными при ошибке
- Работаешь с деньгами, заказами или другими критичными данными
Не используй когда:
- Читаешь данные
- Операции независимы
- Можешь логировать/обновлять асинхронно
Транзакции - это не щит от всех проблем, это инструмент для гарантии консистентности. Используй правильно!