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

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

Ответ

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

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

Суть: блокируем строку для изменения «на входе» в критическую секцию. Другие транзакции будут ждать ее завершения.

-- В начале транзакции блокируем запись для обновления
BEGIN TRANSACTION;

SELECT * FROM accounts WHERE id = 123 FOR UPDATE; -- Блокировка в MySQL/PostgreSQL
-- или SELECT ... WITH (UPDLOCK, ROWLOCK) в SQL Server

-- ... Логика изменения баланса ...
UPDATE accounts SET balance = balance - 100 WHERE id = 123;

COMMIT;
-- Блокировка снимается после COMMIT

Когда использовать: Когда конфликты очень вероятны, а простои из-за ожидания блокировки допустимы.

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

Суть: не блокируем запись заранее, но при обновлении проверяем, что данные не изменились с момента их чтения. Обычно реализуется через колонку версии (version) или временной метки.

-- Предположим, у таблицы `accounts` есть колонка `version`
-- 1. Чтение данных
SELECT id, balance, version FROM accounts WHERE id = 123;
-- Получаем: balance = 1000, version = 5

-- 2. Обновление с проверкой версии
UPDATE accounts 
SET balance = 900, version = version + 1 
WHERE id = 123 AND version = 5; -- Ключевая проверка!

-- 3. Проверка результата
-- Если число затронутых строк (affected rows) = 0, значит,
-- кто-то другой уже изменил запись (версия изменилась).
-- Нужно откатить операцию и повторить логику заново или сообщить об ошибке.

Когда использовать: Когда конфликты редки. Этот подход обеспечивает лучшую производительность, так как не создает долгих блокировок.

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

Многие операции можно выполнить одним атомарным запросом, что исключает гонку.

-- Вместо: 1) SELECT balance, 2) CALCULATE, 3) UPDATE
-- Делаем так:
UPDATE accounts SET balance = balance - 100 WHERE id = 123;
-- СУБД сама обеспечит атомарность этого изменения.

Выбор стратегии зависит от конкретного сценария: частоты конфликтов, требований к производительности и согласованности данных.