← Назад к вопросам

Как на примере доступа сущности к базе не допустить состояния гонки?

3.0 Senior🔥 122 комментариев
#Базы данных и SQL

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Предотвращение состояния гонки при доступе сущности к базе данных

Состояние гонки (race condition) возникает, когда несколько процессов или потоков одновременно обращаются к общим ресурсам (например, записям в БД) и конечный результат зависит от порядка выполнения операций. В контексте PHP Backend-разработки это особенно актуально, учитывая высокую параллельность веб-приложений.

Основные стратегии предотвращения

1. Транзакции с подходящим уровнем изоляции

START TRANSACTION;
-- Операции с данными
COMMIT;

Уровни изоляции в MySQL/PostgreSQL:

  • READ COMMITTED - предотвращает "грязное" чтение
  • REPEATABLE READ (по умолчанию в MySQL) - предотвращает неповторяемое чтение
  • SERIALIZABLE - полная изоляция, но снижает производительность
// Пример с PDO в PHP
$pdo->beginTransaction();
$pdo->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$pdo->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$pdo->commit();

2. Оптимистическая блокировка (Optimistic Locking)

Использует версионирование записей:

UPDATE products 
SET quantity = quantity - 1, version = version + 1 
WHERE id = 123 AND version = 5;
// PHP реализация
public function updateProductQuantity($id, $currentVersion) {
    $stmt = $this->pdo->prepare("
        UPDATE products 
        SET quantity = quantity - 1, 
            version = version + 1,
            updated_at = NOW()
        WHERE id = :id 
        AND version = :version
    ");
    
    $stmt->execute([':id' => $id, ':version' => $currentVersion]);
    
    if ($stmt->rowCount() === 0) {
        throw new OptimisticLockException("Запись была изменена другим процессом");
    }
}

3. Пессимистическая блокировка (Pessimistic Locking)

SELECT * FROM orders WHERE id = 456 FOR UPDATE;
-- или в SQL Server: WITH (UPDLOCK, ROWLOCK)
// Пример в транзакции
$pdo->beginTransaction();
$stmt = $pdo->prepare("SELECT * FROM inventory WHERE product_id = ? FOR UPDATE");
$stmt->execute([$productId]);
// ... логика обработки
$pdo->commit();

4. Атомарные операции UPDATE

Использование атомарных операций на уровне базы данных:

UPDATE counters 
SET value = value + 1 
WHERE id = 1;

UPDATE seats 
SET status = 'occupied', 
    user_id = 123,
    reserved_at = NOW()
WHERE id = 456 
AND status = 'available';

Практические паттерны для PHP Backend

Паттерн "Единый источник истины"

Все модификации данных должны проходить через централизованные сервисы:

class InventoryService {
    private $db;
    private $lockManager;
    
    public function reserveItem($itemId, $quantity) {
        $this->db->beginTransaction();
        
        try {
            // Проверка и блокировка
            $item = $this->lockManager->acquireLock('inventory', $itemId);
            
            if ($item->available < $quantity) {
                throw new InsufficientStockException();
            }
            
            // Атомарное обновление
            $this->db->execute(
                "UPDATE inventory 
                 SET available = available - ?,
                     reserved = reserved + ?
                 WHERE id = ? AND available >= ?",
                [$quantity, $quantity, $itemId, $quantity]
            );
            
            $this->db->commit();
            $this->lockManager->releaseLock('inventory', $itemId);
            
        } catch (Exception $e) {
            $this->db->rollBack();
            $this->lockManager->releaseLock('inventory', $itemId);
            throw $e;
        }
    }
}

Использование очередей для сериализации операций

// RabbitMQ или Redis Queue пример
class OrderProcessor {
    public function processOrder($orderId) {
        Redis::lpush('order_queue', json_encode([
            'order_id' => $orderId,
            'timestamp' => microtime(true)
        ]));
    }
    
    public function worker() {
        while ($job = Redis::brpop('order_queue', 30)) {
            $data = json_decode($job[1], true);
            $this->processSingleOrder($data['order_id']);
        }
    }
}

Дополнительные меры безопасности

  1. Уникальные ограничения на уровне БД:

    ALTER TABLE user_emails ADD UNIQUE (email);
    
  2. Повторные попытки (Retry Logic):

    $maxAttempts = 3;
    $attempt = 0;
    
    while ($attempt < $maxAttempts) {
        try {
            $this->processWithLocking();
            break;
        } catch (OptimisticLockException $e) {
            $attempt++;
            if ($attempt === $maxAttempts) throw $e;
            usleep(100 * $attempt); // Экспоненциальная backoff
        }
    }
    
  3. Таймауты для блокировок:

    SET innodb_lock_wait_timeout = 5; -- MySQL
    SET lock_timeout = '5s'; -- PostgreSQL
    

Рекомендации для разных сценариев

  • Финансовые операции: Используйте SERIALIZABLE уровень изоляции + пессимистические блокировки
  • Инвентаризация: Оптимистическая блокировка с версионированием
  • Системы бронирования: Атомарные UPDATE с проверкой условий в WHERE
  • Счетчики/лайки: Атомарные инкременты без транзакций
  • Очереди задач: Внешние системы (RabbitMQ, Kafka) или SELECT FOR UPDATE

Мониторинг и отладка

-- Мониторинг блокировок в MySQL
SHOW ENGINE INNODB STATUS;
SELECT * FROM information_schema.INNODB_TRX;
SELECT * FROM information_schema.INNODB_LOCKS;

Ключевой принцип: чем меньше времени удерживается блокировка, тем лучше для производительности. Всегда балансируйте между согласованностью данных и производительностью системы, выбирая подходящую стратегию для конкретного бизнес-сценария.