> Зачем нужны блокировки в базе данных (PHP)

Уровень: senior · Роль: backend · Язык: PHP · Категория: Технические вопросы

Компании: vanta

Стек: PHP, Go

> Пример ответа

Короткий ответ

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

Подробное объяснение

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

  • Потерянное обновление - две транзакции читают одно значение, обе меняют, последняя запись затирает первую.
  • Грязное чтение - чтение данных, которые ещё не закоммичены и могут быть откачены.
  • Неповторяющееся чтение - в рамках одной транзакции одно и то же чтение даёт разные результаты.
  • Фантомы - при повторном запросе появляются новые строки, добавленные другой транзакцией.

Уровни изоляции (READ COMMITTED, REPEATABLE READ, SERIALIZABLE) реализуются именно через блокировки или MVCC (multiversion concurrency control). В PostgreSQL и MySQL InnoDB используется MVCC, но блокировки всё равно применяются для write-write конфликтов и явных операций SELECT ... FOR UPDATE.

Блокировки бывают:

  • Разделяемые (shared, S) - несколько читателей могут держать одновременно, писатели - нет.
  • Эксклюзивные (exclusive, X) - только один держатель, никто другой не может ни читать, ни писать.
  • На уровне строк, страниц, таблиц, БД - чем грубее гранулярность, тем меньше накладные расходы, но больше конфликтов.
  • Оптимистичные и пессимистичные - первые проверяют конфликт при коммите, вторые блокируют заранее.

Важный trade-off: блокировки увеличивают время ожидания и риск deadlock'ов. Deadlock - когда две транзакции ждут блокировки друг друга. СУБД автоматически детектит и откатывает одну из них.

На практике

В PHP и Go при работе с БД блокировки используются в двух сценариях:

  1. Явные блокировки строк через SELECT ... FOR UPDATE - когда нужно атомарно прочитать и обновить данные в рамках бизнес-операции. Например, списание средств с баланса: без блокировки два параллельных запроса могут прочитать один и тот же баланс и оба записать уменьшенное значение.

  2. Неявные блокировки при записи - каждая UPDATE или DELETE берёт эксклюзивную блокировку на строку. Проблемы возникают, когда транзакция долго держит блокировку, например, при внешних HTTP-вызовах внутри транзакции.

В Go с database/sql и pgx важно помнить, что транзакция открывается явно, и блокировки держатся до commit/rollback. В PHP с PDO аналогично.

Типичный паттерн - optimistic locking через версионирование: добавляется колонка version, при обновлении проверяется WHERE version = ?, и если затронуто 0 строк - конфликт. Это дешевле пессимистичных блокировок для read-heavy нагрузок.

Пример кода

Пессимистичная блокировка строки в Go с pgx:

GO
tx, err := db.Begin(ctx)
if err != nil { /* handle */ }
defer tx.Rollback(ctx)
var balance int
err = tx.QueryRow(ctx,
"SELECT balance FROM accounts WHERE id = $1 FOR UPDATE",
accountID,
).Scan(&balance)
if err != nil { /* handle */ }
newBalance := balance - amount
_, err = tx.Exec(ctx,
"UPDATE accounts SET balance = $1 WHERE id = $2",
newBalance, accountID,
)
if err != nil { /* handle */ }
tx.Commit(ctx)

Оптимистичная блокировка в PHP с PDO:

PHP
$pdo->beginTransaction();
$stmt = $pdo->prepare("SELECT balance, version FROM accounts WHERE id = ?");
$stmt->execute([$accountId]);
$row = $stmt->fetch();
$newBalance = $row['balance'] - $amount;
$newVersion = $row['version'] + 1;
$update = $pdo->prepare(
"UPDATE accounts SET balance = ?, version = ? WHERE id = ? AND version = ?"
);
$update->execute([$newBalance, $newVersion, $accountId, $row['version']]);
if ($update->rowCount() === 0) {
$pdo->rollBack();
throw new RuntimeException('Конфликт версий, повторите операцию');
}
$pdo->commit();

Как отвечать на собеседовании

Начни с цели - согласованность при конкурентном доступе. Затем перечисли аномалии, которые блокировки предотвращают. Упомяни уровни изоляции и связь с MVCC. Покажи понимание trade-off: блокировки vs производительность, deadlock'и. Приведи конкретный пример из практики - списание средств или заказ товара. Для senior важно показать, что ты знаешь, когда использовать пессимистичные блокировки, а когда оптимистичные, и как избегать долгих транзакций.

Что проверяет интервьюер

  • Понимание модели изоляции транзакций и аномалий.
  • Умение выбирать между пессимистичным и оптимистичным подходом.
  • Знание гранулярности блокировок и их влияния на производительность.
  • Осведомлённость о deadlock'ах и способах их обработки.
  • Практический опыт: как блокировки ведут себя в конкретной СУБД (PostgreSQL, MySQL).
  • Понимание, что блокировки - не серебряная пуля, и есть альтернативы (MVCC, атомарные операции, очереди).

Типичные ошибки

  • Путаница между блокировками и транзакциями - блокировки не заменяют транзакции, а работают внутри них.
  • Утверждение, что SELECT не блокирует ничего - в зависимости от уровня изоляции и СУБД это не так.
  • Игнорирование deadlock'ов - "у нас их нет" обычно значит "мы не настроили таймауты и не логируем".
  • Использование SELECT ... FOR UPDATE без необходимости - это убивает параллельность на read-heavy операциях.
  • Держать транзакцию открытой во время внешних вызовов (HTTP, API) - блокировки висят, растёт вероятность конфликтов.
  • Не учитывать разницу между СУБД: в PostgreSQL FOR UPDATE работает иначе, чем в MySQL с InnoDB.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью