Кнопка «Сохранить» может сообщить об успехе и одновременно уничтожить чужую работу. Сотрудник открыл карточку оборудования, поменял ответственное подразделение и ушёл согласовывать детали. Коллега тем временем исправил адрес размещения. Если первый сотрудник позже отправит всю старую карточку целиком, новое подразделение сохранится, а исправленный адрес вернётся к прежнему значению.
Это потерянное обновление. Достаточно двух людей и паузы между чтением и записью. Стратегия конкуренции определяет, когда останавливать изменение и что предлагать вместо тихой перезаписи.
Проверить версию в момент сохранения
При оптимистичном контроле карточка получает номер версии. Допустим, сотрудник открыл версию 12, а коллега уже сохранил версию 13. Запись первого сотрудника разрешается только при условии, что в базе всё ещё версия 12. Если условие не выполнено, приложение получает конфликт и не перезаписывает чужие данные.
Условие и изменение должны быть одной атомарной операцией. Отдельно прочитать номер, убедиться, что он подходит, а потом выполнить безусловное сохранение — значит снова оставить окно для гонки. Важен и охват: импорт, административный скрипт и фоновый процесс должны обновлять тот же признак версии, иначе часть изменений останется невидимой для проверки.
Оптимистичный подход не означает, что база вообще не использует блокировок. Он позволяет не держать длительную блокировку с момента открытия карточки до её сохранения. Короткая запись в базе всё равно подчиняется её правилам конкуренции. Это различие существенно, когда интерфейс открыт у человека несколько минут.
Вместо номера можно использовать другой маркер изменения. У обычного времени сохранения нужно проверить точность и способ генерации: две разные записи не должны незаметно получить одинаковую отметку.
Обнаруженный конфликт ещё нужно разрешить
Сообщение «не удалось сохранить» мало помогает сотруднику, который только что заполнил длинную форму. Хороший интерфейс сохраняет его ввод и показывает, что изменилось в базе. После этого можно перечитать карточку, перенести отдельные изменения или явно выбрать нужный вариант.
Если два человека правили независимые поля, объединение иногда возможно автоматически. Но независимость нельзя выводить только из разных названий колонок. Подразделение и адрес могут быть связаны правилом размещения оборудования; скидка и итоговая цена — расчётом. Объединённую запись всё равно надо проверить целиком.
Особенно неудачен «умный» повтор: приложение молча получает новую версию и записывает поверх неё прежнее содержимое формы. Формально ошибка исчезла, фактически защита от потери обновлений была обойдена. Для решений, принятых человеком по старым данным, повтор часто требует нового согласия.
Когда разумнее занять ресурс на короткое время
Пессимистичный подход уместен, например, при назначении единственной свободной единицы оборудования. Короткая транзакция получает блокировку подходящей записи, проверяет актуальный статус, назначает объект и завершается. Конкурирующие операции, которым нужна несовместимая блокировка, ожидают или прекращаются по заданному правилу.
В PostgreSQL блокировка строки через SELECT FOR UPDATE не запрещает обычное чтение этой строки, но мешает конфликтующим изменениям до окончания транзакции. Поэтому утверждение «заблокировали — никто не может посмотреть» неверно. Конкретный режим блокировки определяет, кто именно должен ждать.
Открытую форму пользователя не стоит превращать в такую транзакцию. Если продукту нужна возможность «занять карточку на редактирование», это отдельное прикладное соглашение: срок действия, продление, освобождение после ухода пользователя, поведение при просроченном сохранении. Оно не должно бесконечно удерживать соединение и блокировку базы.
Частые столкновения не всегда лечатся сменой стратегии
Два коротких процесса могут заблокировать ресурсы в разном порядке: первый занял оборудование и ждёт запись заявки, второй занял заявку и ждёт оборудование. Получается взаимная блокировка — deadlock. База может обнаружить цикл и отменить одну транзакцию. Приложению всё равно нужны корректная обработка ошибки и, когда это допустимо, повтор всей операции.
Единый порядок получения блокировок, короткие транзакции и ограниченное ожидание уменьшают риск. Но простое увеличение таймаута не устраняет цикл. Оно может лишь заставить людей дольше смотреть на неподвижный экран.
При оптимистичном подходе перегруженная запись даёт другую картину: многие попытки проигрывают проверку версии и начинают работу заново. Повторы расходуют ресурсы и способны поддерживать перегрузку. Иногда разумно сократить объём общей записи, разделить независимые операции или изменить порядок обработки, а не переводить всё приложение на другой вид блокировок.
Высокая цена ошибки сама по себе не делает пессимистичную блокировку обязательной. Корректное условное обновление тоже способно защитить критичное изменение. Выбор зависит от того, как часто сталкиваются операции, сколько длится защищённая работа и можно ли безопасно повторить решение.
Что проверить в работающем продукте
Для карточки оборудования нужны два открытых окна с одной исходной версией. После сохранения во втором окне первое не должно незаметно затереть результат. При этом сотрудник не должен потерять свой черновик. Для автоматического назначения ресурса нужен отдельный тест одновременных запросов и остановки процесса, уже удерживающего блокировку.
Измерять полезно долю конфликтов, длительность ожидания, взаимные блокировки, число повторов и время до окончательного результата. Если пользователи постоянно сталкиваются на одной карточке, техническая защита работает, но сам процесс, возможно, организован неудачно.
И ещё одна граница: блокировки координируют доступ к состоянию, а идемпотентность не даёт повторно выполнить одно поручение. Повторная доставка той же команды после освобождения блокировки может снова пройти, если идентичность операции не проверяется. В критичном процессе нередко нужны обе защиты.