Что такое оптимистичная блокировка?

«Что такое оптимистичная блокировка?» — вопрос из категории Базы данных, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Оптимистичная блокировка (Optimistic Concurrency Control) — это стратегия для работы с параллельным доступом к данным в БД, которая предполагает, что конфликты при одновременном изменении одной записи маловероятны. Вместо явной блокировки записи на время транзакции, конфликт обнаруживается в момент фиксации изменений и должен быть обработан приложением.

Принцип работы:

  1. Каждая запись в таблице имеет поле-версию (обычно version или updated_at).
  2. Клиент, читая запись, также получает текущее значение версии.
  3. При обновлении запись модифицируется только если её версия в БД совпадает с той, что была получена клиентом. Версия увеличивается.
  4. Если версии не совпали (запись уже обновил кто-то другой), операция обновления не затрагивает ни одной строки, и приложение получает сигнал о конфликте.

Пример реализации в Node.js с PostgreSQL и node-postgres:

async function updateProduct(productId, newPrice, clientVersion) {
  const query = `
    UPDATE products 
    SET price = $1, version = version + 1, updated_at = NOW()
    WHERE id = $2 AND version = $3
    RETURNING id, price, version;
  `;
  const values = [newPrice, productId, clientVersion];

  const result = await pool.query(query, values);

  if (result.rowCount === 0) {
    // Конфликт версий: запись не найдена с указанной версией
    // Здесь можно либо выбросить ошибку, либо реализовать повторную попытку
    const currentRecord = await pool.query('SELECT * FROM products WHERE id = $1', [productId]);
    throw new Error(`Конфликт оптимистичной блокировки. Текущая версия: ${currentRecord.rows[0].version}. Ваша версия: ${clientVersion}.`);
  }

  return result.rows[0]; // Возвращаем обновленную запись
}

// Использование
// 1. Сначала читаем продукт: const product = await pool.query('SELECT id, price, version FROM products WHERE id=123');
// 2. Пытаемся обновить: try { await updateProduct(123, 99.99, product.rows[0].version); } catch(err) { /* Обработка конфликта */ }

Плюсы:

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

Минусы:

  • Требует дополнительного поля в таблице.
  • Приложение должно быть готово обрабатывать ошибки конфликтов (например, предлагать пользователю обновить данные и повторить действие).
  • Не подходит для сценариев, где конфликты записи происходят очень часто.

В экосистеме Node.js эта стратегия часто реализуется на уровне ORM (например, в Sequelize есть опция version).