Ответ
При работе с параллельными запросами к одним данным я использую несколько подходов в зависимости от требований к согласованности и производительности.
1. Пессимистичная блокировка (Pessimistic Locking) Используется, когда конфликты часты. Блокируем данные на время транзакции.
// MySQL/PostgreSQL с FOR UPDATE
$pdo->beginTransaction();
try {
// Блокируем строку для обновления
$stmt = $pdo->prepare(
"SELECT balance FROM accounts WHERE id = ? FOR UPDATE"
);
$stmt->execute([$accountId]);
$balance = $stmt->fetchColumn();
// Проверка бизнес-логики
if ($balance < $amount) {
throw new Exception('Недостаточно средств');
}
// Обновление
$updateStmt = $pdo->prepare(
"UPDATE accounts SET balance = balance - ? WHERE id = ?"
);
$updateStmt->execute([$amount, $accountId]);
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
2. Оптимистичная блокировка (Optimistic Locking) Подходит для редких конфликтов. Проверяем версию при обновлении.
// Таблица с версией
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(100),
quantity INT,
version INT DEFAULT 0
);
// PHP код
$pdo->beginTransaction();
// Читаем текущее состояние
$stmt = $pdo->prepare(
"SELECT id, quantity, version FROM products WHERE id = ?"
);
$stmt->execute([$productId]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
// Бизнес-логика
$newQuantity = $product['quantity'] - $ordered;
// Обновляем только если версия не изменилась
$updateStmt = $pdo->prepare(
"UPDATE products
SET quantity = ?, version = version + 1
WHERE id = ? AND version = ?"
);
$affected = $updateStmt->execute([
$newQuantity,
$productId,
$product['version']
]);
if ($affected === 0) {
// Кто-то уже обновил данные - повторяем операцию
$pdo->rollBack();
// Повторная попытка или уведомление пользователя
throw new OptimisticLockException('Данные были изменены');
}
$pdo->commit();
3. Уровни изоляции транзакций
// MySQL: SET TRANSACTION ISOLATION LEVEL
$pdo->exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
$pdo->beginTransaction();
// Или в PostgreSQL
$pdo->beginTransaction();
$pdo->exec("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
4. Очереди для сериализации запросов Для высоконагруженных систем:
// Использование Redis для очереди
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// Блокировка по ключу
$lockKey = "lock:account:$accountId";
$lockTimeout = 10; // секунд
if ($redis->set($lockKey, 1, ['nx', 'ex' => $lockTimeout])) {
try {
// Выполняем операцию
$pdo->beginTransaction();
// ... логика обновления ...
$pdo->commit();
} finally {
$redis->del($lockKey);
}
} else {
throw new Exception('Операция уже выполняется');
}
Выбор стратегии:
- Пессимистичная блокировка: банковские операции, бронирование
- Оптимистичная блокировка: корзины покупок, редактирование контента
- Очереди: массовые операции, фоновые задачи
- Уровни изоляции: зависит от требований ACID и производительности