Ответ
В высоконагруженных системах я отдаю предпочтение оптимистичным блокировкам (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) — использую только в крайних случаях, например, для финансовых операций, где конфликт вероятен и его стоимость высока, и то стараюсь максимально сужать область и время блокировки.