Два клиента пытаются купить товар в одно и то же время.
Если приложение не рассчитано на параллелизм (concurrency), оба запроса могут прочитать stock = 1 и оба успешно завершат покупку.
Именно здесь важную роль играют атомарность, транзакции и блокировки.
1. Атомарность
Атомарность — это одно из четырех свойств ACID:
- Atomicity (Атомарность)
- Consistency (Согласованность)
- Isolation (Изолированность)
- Durability (Надежность)
Атомарность означает, что группа операций с базой данных рассматривается как единый не неделимый элемент.
Ибо:
- все операции завершаются успешно, либо
- все операции откатываются назад.
Пример
Order::create([
'user_id' => 1,
'total' => 100,
]);
Payment::create([
'user_id' => 1,
'amount' => 100,
]);
Если заказ создан, но платеж не прошёл, мы можем получить несогласованное состояние данных.
Транзакция позволяет нам сделать эти операции атомарными.
2. Транзакции
Laravel предоставляет:
DB::transaction(function () {
// database operations
});
Пример:
DB::transaction(function () {
$order = Order::create([
'user_id' => 1,
'total' => 100,
]);
Payment::create([
'user_id' => 1,
'amount' => 100,
]);
});
Если всё прошло успешно:
COMMIT
Если возникло исключение:
ROLLBACK
Главная идея заключается в следующем:
Транзакция объединяет связанные операции базы данных в единую рабочую единицу (unit of work).
3. Состояние гонки (Race Conditions)
Состояние гонки (race condition) возникает, когда несколько запросов одновременно обращаются к общим данным, и результат зависит от последовательности и времени выполнения этих операций.
Представьте:
Product stock = 1
Поступают два запроса:
Request A Request B
--------- ---------
Read stock = 1 Read stock = 1
Stock available Stock available
Buy product Buy product
Оба запроса увидели одно и то же значение остатка на складе.
Это может привести к некорректному поведению системы.
4. Пессимистичная блокировка
Пессимистичная блокировка (pessimistic locking) исходит из предположения, что конфликт вероятен.
Идея такова:
Заблокируйте данные перед началом работы с ними.
Laravel предоставляет:
lockForUpdate()
и:
sharedLock()
5. lockForUpdate()
lockForUpdate() обычно используется, когда вам нужно прочитать строку, а затем изменить её.
DB::transaction(function () use ($productId) {
$product = Product::where('id', $productId)
->lockForUpdate()
->firstOrFail();
if ($product->stock <= 0) {
throw new Exception('Out of stock.');
}
$product->decrement('stock');
Order::create([
'product_id' => $product->id,
]);
});
Самое важное здесь:
->lockForUpdate()
При обычном поиске по первичному ключу это можно воспринимать как блокировку соответствующей строки.
6. Что происходит при двух параллельных запросах?
Предположим:
Product #1
stock = 1
Запрос A:
BEGIN TRANSACTION
↓
Lock Product #1
↓
Read stock = 1
↓
Decrease stock
↓
Create order
↓
COMMIT
↓
Unlock
Запрос B пытается заблокировать ту же строку, пока у запроса A все еще удержана блокировка:
BEGIN TRANSACTION
↓
Try to lock Product #1
↓
WAIT...
После того как Запрос A выполняет фиксацию (commit), блокировка снимается, и Запрос B может продолжить работу.
Запрос B затем считывает уже обновленный остаток товара.
7. Блокирует ли lockForUpdate() всю таблицу?
Обычно нет.
Для:
$product = Product::where('id', $productId)
->lockForUpdate()
->firstOrFail();
в большинстве случаев заблокированной будет считаться только совпавшая строка.
Например:
products
id stock
------------
1 10 available
2 20 LOCKED
3 30 available
Точное поведение блокировки зависит от движка базы данных, индексов, уровня изоляции и самого SQL-запроса.
Для простого поиска по первичному ключу правильнее всего рассуждать в терминах построчной блокировки (row-level lock).
8. sharedLock()
Laravel также предоставляет:
->sharedLock()
Пример:
DB::transaction(function () {
$product = Product::where('id', 1)
->sharedLock()
->firstOrFail();
// Protected read
});
Наглядная ментальная модель:
sharedLock()
↓
"I'm reading this data and want protected access."
в то время как:
lockForUpdate()
↓
"I'm reading this because I'm going to modify it."
9. Оптимистичная блокировка
Оптимистичная блокировка (optimistic locking) использует другой подход.
Вместо блокировки строки мы предполагаем, что конфликты происходят относительно редко.
Идея такова:
Не блокируйте запись. При попытке обновления просто проверьте, не изменил ли её кто-то другой.
Распространенная реализация использует колонку version .
Пример:
users
id balance version
------------------------
1 1000 5
Приложение считывает:
balance = 1000
version = 5
Позже оно выполняет обновление только в том случае, если версия по-прежнему равна 5 :
UPDATE users
SET balance = 500,
version = 6
WHERE id = 1
AND version = 5;
Если обновлена одна строка, операция прошла успешно.
Если обновлено ноль строк, значит, кто-то другой уже изменил запись.
10. Оптимистичная блокировка в Laravel
В Laravel/Eloquent нет встроенной поддержки оптимистичной блокировки как стандартной фичи «из коробки» (в отличие от lockForUpdate() ).
Вы можете реализовать её самостоятельно.
$user = User::findOrFail($id);
$version = $user->version;
$updated = User::where('id', $user->id)
->where('version', $version)
->update([
'balance' => 500,
'version' => $version + 1,
]);
if ($updated === 0) {
throw new RuntimeException(
'The record was modified by another process.'
);
}
Главный принцип:
Read version
↓
Work
↓
UPDATE ... WHERE version = old_version
↓
Success → nobody changed it
Failure → somebody changed it
11. Пессимистичная и оптимистичная блокировки: сравнение
| Пессимистичная | Оптимистичная | |
|---|---|---|
| Стратегия | Сначала заблокировать | Обнаружить конфликт позже |
| Блокирует параллельный доступ | Да | Нет |
| Блокировка строки в БД | Да | Обычно нет |
| Колонка версии (version) | Не требуется | Обычно нужна |
| Когда применять | Высокая вероятность конфликтов | Редкие конфликты |
| Laravel | lockForUpdate() |
Обычно собственная реализация |
Простой способ запомнить:
Пессимистичная: «Я жду конфликта, поэтому сначала заблокирую запись».
Оптимистичная: «Я не жду конфликта, но если он случится — я его обнаружу».
12. Взаимные блокировки (Deadlocks)
Взаимная блокировка (deadlock) происходит, когда две транзакции ждут снятия блокировок друг от друга.
Например:
Transaction A Transaction B
Lock User #1 Lock User #2
↓ ↓
Try User #2 Try User #1
↓ ↓
WAIT WAIT
Теперь:
A waits for B
B waits for A
База данных обнаруживает дедлок и обычно экстренно прерывает (abort) одну из транзакций.
Как уменьшить количество дедлоков
- Делайте транзакции максимально короткими.
- Блокируйте ресурсы всегда в одинаковом порядке.
- Избегайте лишних блокировок.
- Не выполняйте медленные операции внутри транзакций.
- Повторяйте транзакции (retry) при их сбое, если это уместно.
13. Атомарные обновления
Иногда вам не нужна явная блокировка.
Вместо такого кода:
$product = Product::find($id);
if ($product->stock > 0) {
$product->decrement('stock');
}
вы можете сделать условие и обновление частью одного SQL-запроса:
$updated = Product::where('id', $id)
->where('stock', '>', 0)
->decrement('stock');
if ($updated === 0) {
throw new Exception('Out of stock.');
}
Концептуально это превращается в:
UPDATE products
SET stock = stock - 1
WHERE id = ?
AND stock > 0;
База данных выполняет проверку условия и обновление атомарно (вместе).
14. Cache::lock()
Laravel также предоставляет атомарные блокировки на уровне приложения:
Cache::lock()
Пример:
$lock = Cache::lock("process-order:{$orderId}", 10);
if ($lock->get()) {
try {
// Critical section
} finally {
$lock->release();
}
}
Это отличается от:
lockForUpdate()
Наглядное различие:
lockForUpdate()
↓
Database row locking
Cache::lock()
↓
Application/distributed locking
Cache::lock() полезен, когда нескольким воркерам или серверам нужно скоординировать доступ к одному и тому же логическому ресурсу.
15. Транзакции vs Блокировки
Это различие крайне важно.
Транзакция отвечает на вопрос:
«Какие операции должны быть выполнены или отменены совместно?»
Блокировка отвечает на вопрос:
«Как параллельные операции должны получать доступ к одним и тем же данным?»
Схема для запоминания:
Transaction
↓
Atomicity
↓
COMMIT / ROLLBACK
Lock
↓
Concurrency control
↓
Prevent or detect conflicts
Часто они используются вместе:
DB::transaction(function () use ($productId) {
$product = Product::where('id', $productId)
->lockForUpdate()
->firstOrFail();
if ($product->stock <= 0) {
throw new Exception('Out of stock.');
}
$product->decrement('stock');
Order::create([
'product_id' => $product->id,
]);
});
В данном случае:
-
DB::transaction()обеспечивает атомарность. -
lockForUpdate()обеспечивает пессимистичный контроль конкурентного доступа. - Приложение не позволяет двум запросам купить последний оставшийся товар.
16. Вопросы для собеседований
Что такое атомарность?
Атомарность означает, что группа операций с базой данных обрабатывается как единое целое. Либо все операции фиксируются (commit), либо они полностью откатываются (rollback).
Что такое транзакция?
Транзакция объединяет операции с базой данных в единую группу и предоставляет семантику фиксации (commit) и отката (rollback).
Что такое состояние гонки (race condition)?
Состояние гонки возникает, когда параллельные операции обращаются к общему состоянию, и итоговый результат зависит от времени их выполнения или порядка вызовов.
Что такое lockForUpdate() ?
lockForUpdate()— это механизм пессимистичной блокировки строк в Laravel. Он используется, когда транзакции нужно прочитать и затем изменить строки, предотвращая конфликтные параллельные обновления.
Что такое оптимистичная блокировка?
Оптимистичная блокировка не блокирует параллельный доступ. Вместо этого она определяет, изменилась ли запись между чтением и обновлением, обычно с помощью колонки версии (version).
Есть ли в Laravel встроенная оптимистичная блокировка?
В Eloquent нет стандартной встроенной оптимистичной блокировки, подобной
lockForUpdate(). Обычно её реализуют вручную с помощью проверки версии или временной метки (timestamp).
Что такое взаимная блокировка (deadlock)?
Взаимная блокировка происходит, когда транзакции удерживают блокировки, необходимые другим транзакциям, из-за чего они бесконечно ждут друг друга.
В чем разница между оптимистичной и пессимистичной блокировками?
Пессимистичная блокировка предотвращает конфликты, блокируя ресурс перед началом работы с ним. Оптимистичная блокировка предполагает, что конфликты редки, и выявляет их непосредственно при обновлении.
CONCURRENCY
|
+--------------+--------------+
| |
TRANSACTIONS LOCKS
| |
Atomicity +-------+-------+
| | |
COMMIT/ROLLBACK Pessimistic Optimistic
| |
lockForUpdate() version check
sharedLock()
Главные тезисы, которые стоит запомнить:
Atomicity
↓
All operations succeed or all fail.
Transaction
↓
Groups operations into one unit of work.
Pessimistic locking
↓
Lock first, then work.
Optimistic locking
↓
Work first, detect conflicts when saving.
Race condition
↓
Concurrent operations produce an incorrect or unexpected result.
Deadlock
↓
Transactions wait for each other's locks.
Эти концепции дадут вам прочный фундамент для понимания работы с конкурентностью в БД на собеседовании по Laravel.
Комментарии (0)
Пока нет комментариев — будьте первым.