Ответ
В высоконагруженных системах я отдаю предпочтение оптимистичным блокировкам (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) — использую только в крайних случаях, например, для финансовых операций, где конфликт вероятен и его стоимость высока, и то стараюсь максимально сужать область и время блокировки.
Ответ 18+ 🔞
Слушай, а вот это реально интересная тема, про которую можно поговорить. Представь себе: у тебя система, которая должна держать овердохуища запросов в секунду. И если ты начнёшь там всё подряд на SELECT ... FOR UPDATE вешать, то это будет пиздец, а не архитектура. Все встанут в очередь, как за колбасой в девяностые. Доверия ебать ноль к такому подходу в 2024-м году.
Так вот, я, блядь, за оптимистичные блокировки. Это как в жизни: идешь в бар, видишь свободный стул — садишься. Не стоишь же ты над ним, уставившись в него, пока не вернёшься с туалета, охраняя его от всех, сука? Ты просто садишься. А если придёт другой чувак и скажет "ой, это моё", ну, бывает, извинился, пошёл искать другой. Так и тут. Мы предполагаем, что конфликты — редкость, и действуем на смелости.
Вот смотри, как это выглядит в коде:
Добавляем к записи версию. Это как тату "не трожь" на видном месте.
CREATE TABLE inventory (
id INT PRIMARY KEY,
product_name VARCHAR(255),
quantity INT,
version INT DEFAULT 0 -- Вот наш страж чести
);
А дальше в приложении делаем такую хитрую жопу:
$entityManager->beginTransaction();
// 1. Читаем данные. Запоминаем версию.
$product = $entityManager->find(Product::class, $id);
$initialVersion = $product->getVersion(); // Запомнили, что видели версию 5
// 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 // Обновим ТОЛЬКО если версия всё ещё 5
]);
$affectedRows = $query->execute();
if ($affectedRows === 0) {
// А вот тут нас наёбали! Пока мы думали, кто-то уже всё обновил.
$entityManager->rollback();
throw new OptimisticLockException('Data was modified concurrently'); // Иди нахуй, пробуй заново
}
$entityManager->commit();
В чём соль? Блокировок-то нет! Все читают свободно. Проблема только если два процесса в долю секунды полезли менять одну и ту же строку. Один успеет, а второй получит ни хуя себе и пойдёт перезапрашивать данные и пробовать ещё раз. Для пользователя это часто даже не заметно.
Но это для сложных операций. А если тебе просто лайк поставить или количество товара уменьшить? Ёпта, зачем городить огород? Используй то, что база уже умеет:
UPDATE inventory
SET quantity = quantity - 1
WHERE id = 123 AND quantity > 0; -- Атомарно и без лишних телодвижений
Всё. Вилкой в глаз или в жопу раз. Либо обновилось, либо нет. Красота.
А если нагрузка вообще адская, как на распродажах? Тогда надо выносить мозг, то есть данные, из базы. Redis — наш хуй с горы в таких случаях.
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// Просто, атомарно, без нервов.
$newValue = $redis->incr('product:views:123');
А потом уже фоновыми задачами эти счётчики в основную базу сливаешь. И все довольны.
Итог моего блядского опыта:
- Оптимистичные блокировки — для сложных транзакций, где данные надо пощупать, подумать, потом записать. Волнение ебать, но работает.
- Атомарные операции БД — для простых действий. Идеально.
- In-memory (Redis) — когда терпения ноль ебать и нужно просто летать.
- Пессимистичные блокировки (
SELECT ... FOR UPDATE) — оставляю для особых извращений, вроде финансовых переводов, где ошибка стоит дорого. Но и там стараюсь, чтобы эта блокировка жила меньше, чем таракан под тапком.