Какую стратегию блокировок использовать в очень нагруженной системе?

«Какую стратегию блокировок использовать в очень нагруженной системе?» — вопрос из категории Базы данных, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В высоконагруженных системах я отдаю предпочтение оптимистичным блокировкам (optimistic concurrency control) и бесблокировочным (lock-free) подходам, чтобы минимизировать contention (конкуренцию за ресурсы) и максимизировать пропускную способность.

1. Оптимистичная блокировка (Optimistic Locking): Этот подход исходит из предположения, что конфликты при одновременном изменении одних данных редки. Вместо блокировки строки на время транзакции, мы проверяем, не изменились ли данные с момента их чтения.

Классическая реализация с версией (versioning):

-- Структура таблицы
CREATE TABLE inventory (
    id INT PRIMARY KEY,
    product_name VARCHAR(255),
    quantity INT,
    version INT DEFAULT 0
);
// В коде приложения (например, с использованием Doctrine ORM)
$entityManager->beginTransaction();

// 1. Чтение данных с версией
$product = $entityManager->find(Product::class, $id);
$initialVersion = $product->getVersion();

// 2. Бизнес-логика (работа с объектом в памяти)
$product->setQuantity($product->getQuantity() - $orderedQty);

// 3. Попытка обновления с проверкой версии
$query = $entityManager->createQuery(
    'UPDATE Product p 
     SET p.quantity = :newQty, p.version = p.version + 1 
     WHERE p.id = :id AND p.version = :version'
);
$query->setParameters([
    'newQty' => $product->getQuantity(),
    'id' => $id,
    'version' => $initialVersion
]);

$affectedRows = $query->execute();

if ($affectedRows === 0) {
    // Версия изменилась — кто-то другой обновил запись первым
    $entityManager->rollback();
    throw new OptimisticLockException('Data was modified concurrently');
}

$entityManager->commit();

Преимущества: Нет долгих блокировок, высокая производительность на чтение. Недостатки: Требует обработки конфликтов на уровне приложения (повторные попытки или уведомление пользователя).

2. Бесблокировочные конструкции на уровне БД: Для атомарных операций, таких как уменьшение остатка, лучше использовать возможности самой СУБД, избегая даже коротких блокировок.

-- Атомарное обновление с проверкой
UPDATE inventory 
SET quantity = quantity - 1 
WHERE id = 123 AND quantity > 0;

-- Проверяем, удалось ли обновить
IF ROW_COUNT() = 0 THEN
    -- Товара нет в наличии
END IF;

3. Вынос горячих данных из БД: Для экстремально нагруженных счетчиков (лайки, просмотры) я использовал Redis с его атомарными командами:

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// INCR атомарно увеличивает счетчик
$newValue = $redis->incr('product:views:123');

// Паттерн "Reserve Inventory" в Redis
$luaScript = <<<LUA
    local current = redis.call('GET', KEYS[1])
    if current and tonumber(current) >= tonumber(ARGV[1]) then
        return redis.call('DECRBY', KEYS[1], ARGV[1])
    else
        return -1
    end
LUA;

// Атомарно резервируем 5 единиц товара
$result = $redis->eval($luaScript, ['inventory:product:123', 5], 1);
if ($result == -1) {
    echo 'Not enough stock';
}

Периодически накопленные в Redis данные синхронизируются в основную БД фоновыми заданиями.

Итоговая стратегия:

  • Оптимистичные блокировки — для сложных бизнес-транзакций, где данные читаются и модифицируются в несколько этапов.
  • Атомарные операции СУБД — для простых инкрементов/декрементов.
  • In-memory хранилища (Redis) — для пиковых нагрузок на счетчики и кэширование "горячих" данных.
  • Пессимистичные блокировки (SELECT ... FOR UPDATE) — использую только в крайних случаях, например, для финансовых операций, где конфликт вероятен и его стоимость высока, и то стараюсь максимально сужать область и время блокировки.