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

Ответ

Оптимистичная блокировка (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).

Ответ 18+ 🔞

А, оптимистичная блокировка! Ну это ж классика, ёпта. Представь себе ситуацию: два чувака одновременно лезут редактировать одну и ту же запись в базе. Пессимист сразу навесит замок, чтоб никто не трогал, пока он сам не закончит. А оптимист — он же по жизни верит в лучшее. Он думает: «Да похуй, кто ж будет со мной конфликтовать? Я быстро!». И лезет менять данные без всяких блокировок. Ну а если всё-таки упёрлись лбами — вот тогда уже разбираемся, по факту. Хитрая жопа, но работает.

Как это, блядь, устроено:

  1. К каждой строчке в таблице прикручивают поле-версию. Обычно это version (просто цифра) или updated_at (время последнего обновления). Это как тату «не трожь, я обновлён».
  2. Когда ты читаешь запись, ты заодно хватаешь и эту версию. Запоминаешь, типа «я видел её вот такой».
  3. А когда хочешь её обновить, то пишешь в базу что-то вроде: «Эй, PostgreSQL, обнови эту запись, но ТОЛЬКО если её версия до сих пор та самая, которую я видел. И заодно версию увеличь, чтоб следующий лох понял, что тут уже поработали».
  4. Если за то время, пока ты думал, запись уже успел поменять какой-нибудь другой полупидор, то версии не совпадут. Твоя команда UPDATE нихрена не обновит (ноль строк затронет), и ты получишь сигнал: «Братан, опоздал, тут уже всё поменялось».

Вот как это выглядит в коде на Node.js с PostgreSQL:

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: true, и они сами под капотом эту магию версий провернут. Удобно, конечно, но понимать, как оно работает изнутри — всё равно полезно. А то потом будешь охуевать, почему данные не обновились.