Два клиента пытаются купить товар в одно и то же время.

Если приложение не рассчитано на параллелизм (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.