EssayAI
Блог
Блог
Математика и алгоритмы

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

19 июня 2026Время чтения: 8 минут
#оптимистичная блокировка#пессимистичная блокировка#конкурентный доступ#версионирование#транзакции
Оптимистичная и пессимистичная блокировка: чем отличаются

Когда две транзакции одновременно читают одну и ту же запись, меняют её и пытаются сохранить, одно из изменений рискует молча затереть другое. Это классическая проблема потерянного обновления (lost update). Чтобы её не допустить, в системах с конкурентным доступом применяют две принципиально разные стратегии - оптимистичную и пессимистичную блокировку. Они отличаются не синтаксисом, а самим предположением о том, как часто данные будут конфликтовать. Разберём, как устроена каждая, чем они отличаются и как выбрать подходящую под вашу нагрузку.

Если нужно быстро квалифицировать конкретную ситуацию из задачи или собеседования - соберите запрос в форме ниже и получите пошаговый разбор.

Проблема, которую решают обе стратегии

Представьте, что два пользователя открыли карточку товара с остатком 10 штук. Оба читают значение 10, оба оформляют заказ на 1 штуку, оба пишут обратно 9. В результате продано две единицы, а остаток уменьшился только на одну. Это и есть потерянное обновление: второе сохранение перезаписало результат первого, не зная о нём.

Формально проблема возникает потому, что между чтением и записью проходит время, и за этот промежуток данные могут измениться. Любая блокировка отвечает на один вопрос: что делать с этим промежутком? Пессимистичная стратегия закрывает доступ заранее, оптимистичная - проверяет факт изменения в момент записи.

Схема потерянного обновления: две транзакции читают остаток десять, обе записывают девять, одно изменение затирается
Схема потерянного обновления: две транзакции читают остаток десять, обе записывают девять, одно изменение затирается

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

Пессимистичная блокировка (pessimistic locking) исходит из предположения, что конфликты вероятны, поэтому запись нужно заблокировать сразу при чтении. Транзакция захватывает строку и держит её до своего завершения. Все остальные, кто хочет ту же строку изменить, ждут в очереди.

В реляционных СУБД это делается через явный захват в рамках транзакции:

SELECT * FROM products WHERE id = 42 FOR UPDATE;

После FOR UPDATE строка с id = 42 заблокирована: любая другая транзакция с тем же FOR UPDATE будет ждать, пока первая не сделает COMMIT или ROLLBACK. Это гарантирует, что между чтением и записью никто значение не тронет.

Плюс очевиден: конфликт физически невозможен, логика проще - прочитал, поменял, сохранил, отпустил. Минус - цена за эту гарантию. Пока строка заблокирована, остальные простаивают. На высоконагруженной системе с горячими записями (популярный товар, общий счётчик) очередь блокировок становится узким местом. Хуже того, при неаккуратном порядке захвата нескольких строк возникает взаимная блокировка - две транзакции ждут ресурсы друг друга и не двигаются.

Пессимистичная блокировка держит ресурс всё время транзакции. Если внутри транзакции есть медленный внешний вызов (запрос к API, ожидание пользователя), вы блокируете строку на всё это время и убиваете параллелизм.

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

Оптимистичная блокировка (optimistic locking) исходит из противоположного предположения: конфликты редки, поэтому блокировать заранее расточительно. Транзакция читает данные без всякой блокировки, спокойно их меняет, а проверку делает только в момент записи - не изменил ли кто-то запись за это время.

Технически это реализуется через версионирование. К строке добавляют поле версии (целое число или метку времени). При чтении запоминают текущую версию, при записи сравнивают её с тем, что в базе:

UPDATE: vпрочитанная=vв базе    commit,vпрочитаннаяvв базе    конфликт\text{UPDATE: } v_{\text{прочитанная}} = v_{\text{в базе}} \;\Rightarrow\; \text{commit}, \quad v_{\text{прочитанная}} \neq v_{\text{в базе}} \;\Rightarrow\; \text{конфликт}

На уровне SQL версия включается в условие обновления:

UPDATE products
SET stock = 9, version = version + 1
WHERE id = 42 AND version = 7;

Если запрос обновил одну строку - всё прошло, версия совпала, мы первыми. Если ноль строк - значит, кто-то уже увеличил версию между нашим чтением и записью. Это и есть обнаруженный конфликт: данные под нами поменялись.

Схема версионного флага: при чтении версия семь, перед записью сверяется версия в базе, при несовпадении возникает конфликт
Схема версионного флага: при чтении версия семь, перед записью сверяется версия в базе, при несовпадении возникает конфликт

Главное достоинство оптимистичной стратегии - отсутствие блокировок и, как следствие, высокий параллелизм при чтении. Цена - необходимость обрабатывать конфликт, когда он всё-таки случился: повторить операцию (retry), показать пользователю «данные изменились, обновите страницу» или слить изменения вручную.

Чем отличаются: ключевая таблица

Чтобы зафиксировать разницу, сведём стратегии в сопоставление по основным критериям.

  • Когда срабатывает защита. Пессимистичная - при чтении (захватывает заранее). Оптимистичная - при записи (проверяет постфактум).
  • Предположение о конфликтах. Пессимистичная ждёт частых конфликтов, оптимистичная - редких.
  • Цена. Пессимистичная платит ожиданием в очереди и риском дедлоков. Оптимистичная платит откатами и повторами при конфликте.
  • Параллелизм. У оптимистичной он выше: читать можно сколько угодно потоков без блокировок.
  • Сложность кода. Пессимистичная проще (нет ветки обработки конфликта), оптимистичная требует retry-логики.

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

Когда какую выбирать

Выбор не идеологический, а количественный - он зависит от частоты конфликтов на вашей нагрузке.

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

Пессимистичная блокировка оправдана там, где конфликты - норма, а откат дорог: списание со счёта, бронирование последнего места, инкремент общего счётчика под высокой нагрузкой. Здесь оптимистичная стратегия будет генерировать столько повторов, что суммарно проиграет по производительности обычной очереди блокировок.

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

Реализация в популярных ORM

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

  • JPA/Hibernate - аннотация поля @Version включает оптимистичную проверку автоматически; при конфликте летит OptimisticLockException. Для пессимистичной - LockModeType.PESSIMISTIC_WRITE (транслируется в SELECT ... FOR UPDATE).
  • Django ORM - оптимистичная делается вручную через filter(version=v).update(...) и проверку числа затронутых строк; пессимистичная - select_for_update().
  • SQLAlchemy - version_id_col для версионирования; with_for_update() для захвата строки.

Принцип везде один: версионный столбец и проверка числа изменённых строк - это оптимистичная стратегия; FOR UPDATE и его обёртки - пессимистичная.

Частые ошибки

  • Забыли обработать конфликт при оптимистичной блокировке. Запрос обновил ноль строк, а код считает это успехом. Изменение молча теряется. Всегда проверяйте число затронутых строк и реагируйте на ноль.
  • Бесконечный retry без ограничения. Повтор при конфликте без лимита попыток и без паузы превращается в зацикливание на горячей строке. Ограничивайте число повторов.
  • Пессимистичная блокировка с долгой транзакцией. Захватили строку и внутри той же транзакции пошли в медленный внешний сервис - заблокировали ресурс на секунды. Блокировку держите минимально.
  • Разный порядок захвата строк. Две транзакции берут строки A и B в разном порядке - классический рецепт взаимной блокировки. Захватывайте ресурсы в едином детерминированном порядке.
  • Путают изоляцию транзакций и блокировку. Уровень изоляции и стратегия блокировки решают пересекающиеся, но разные задачи. Оптимистичная блокировка не заменяет корректный уровень изоляции.

FAQ

Можно ли использовать оптимистичную блокировку без отдельного поля версии? Да, проверку можно строить на самих изменяемых полях: включить в условие UPDATE старые значения всех полей, которые читали. Но это хрупко при большом числе колонок и не ловит конфликт по полям, которые вы не трогали. Отдельный счётчик версии надёжнее и читаемее.

Оптимистичная блокировка - это вообще блокировка? Физически - нет, в момент чтения и изменения ничего не блокируется, потоки работают параллельно. «Блокировкой» её называют по аналогии: она тоже предотвращает потерянное обновление, но не удержанием ресурса, а проверкой версии при записи. Иногда её честнее называть оптимистичным контролем конкуренции.

Что лучше для высоконагруженной системы? Зависит от того, конкурируют ли потоки за одни и те же строки. Если нагрузка размазана по многим записям - оптимистичная даёт больший параллелизм. Если все бьются в одну горячую строку - пессимистичная очередь предсказуемее, чем шторм откатов. Замеряйте долю конфликтов, а не выбирайте по интуиции.

Коротко

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

Доверьте текст нейросети EssayAI

Открыть EssayAI

Бесплатно, на русском языке и без VPN

Читайте также