11 июн. 2025 г.·7 мин

Оптимистичная блокировка для админ‑инструментов: предотвращайте тихие перезаписи

Изучите оптимистичную блокировку для админ‑инструментов: колонка версии и проверка updated_at, а также простые UI‑паттерны для обработки конфликтов редактирования без тихих перезаписей.

Оптимистичная блокировка для админ‑инструментов: предотвращайте тихие перезаписи

Проблема: тихие перезаписи при одновременной работе

«Тихая перезапись» случается, когда два человека открывают одну и ту же запись, оба вносят изменения, а последний, кто нажал Сохранить, побеждает. Правки первого человека исчезают без предупреждения и часто без простого способа восстановления.

В загруженной админ-панели это может происходить постоянно, пока никто не заметит. Люди держат несколько вкладок, переключаются между задачами и возвращаются к форме, которая простаивает 20 минут. Когда они, наконец, сохраняют, они уже не редактируют актуальную версию записи — они её перезаписывают.

Это чаще встречается в внутренних инструментах, чем в публичных приложениях, потому что работа коллективная и основана на общих записях. Внутренние команды многократно правят одних и тех же клиентов, заказы, продукты и запросы, часто в коротких сессиях. Публичные приложения чаще — «пользователь редактирует свои данные», а админ‑инструменты — это «много людей правят общие записи».

Последствия редко драматичны в моменте, но накапливаются быстро:

  • Цена товара возвращается к старому значению сразу после обновления для промо.
  • Внутренняя заметка агента поддержки исчезает, и следующий агент повторяет те же шаги.
  • Статус заказа откатывается назад (например, «Отправлен» обратно в «Упакован»), что вызывает неверную обработку.
  • Номер телефона или адрес клиента заменяются устаревшей информацией.

Тихие перезаписи болезненны, потому что все думают, что система корректно сохранила данные. Нет явного «что-то пошло не так», зато появляется путаница, когда отчёты не сходятся или коллеги спрашивают: «Кто это поменял?»

Конфликты — нормальная часть совместной работы. Это знак того, что инструмент действительно используется, а не ошибка команды. Цель не в том, чтобы запретить двум людям редактировать одновременно, а в том, чтобы обнаружить, что запись изменилась пока кто‑то её редактировал, и корректно обработать этот момент.

Если вы строите внутренний инструмент на no-code платформе, например AppMaster, стоит заложить это с самого начала. Админ-инструменты быстро растут, и как только команды начинают на них полагаться, потеря данных «иногда» превращается в постоянный источник недоверия.

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

Когда два человека открывают одну запись и оба нажимают Сохранить, возникает конкурентность. Каждый стартовал с более старой копии, но в итоге только одно сохранение может стать «последним».

Без защиты побеждает последнее сохранение — так и появляются тихие перезаписи: второе сохранение незаметно заменяет изменения первого.

Оптимистичная блокировка — простое правило: «Я сохраню изменения только если запись всё ещё в том же состоянии, в котором я её открыл.» Если запись изменилась, сохранение отклоняется, и пользователь видит конфликт.

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

Оптимистичная блокировка обычно лучше по умолчанию, потому что рабочий поток не останавливается. Люди могут редактировать параллельно, а система вмешивается только при реальном столкновении.

Она подходит когда:

  • конфликты возможны, но не постоянны;
  • правки небольшие (несколько полей, короткая форма);
  • блокировка других замедлит команду;
  • вы можете показать понятное сообщение «кто‑то обновил запись»;
  • ваш API может проверять версию (или метку времени) при каждом обновлении.

Что она предотвращает — проблему «тихой перезаписи». Вместо потери данных вы получаете ясную остановку: «Эта запись изменилась с тех пор, как вы её открыли.»

Что она не делает тоже важно. Она не запрещает двум людям принять разные, но корректные решения на основе старых данных, и она не умеет автоматически сливать изменения. И если вы пропустите проверку на стороне сервера, вы ничего не решили.

Ограничения на практике:

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

На практике оптимистичная блокировка — просто дополнительное значение, которое сопровождает правку, плюс правило на сервере «обновить только если совпадает». В AppMaster такая проверка обычно живёт в бизнес-логике там, где выполняется обновление.

Два общих подхода: колонка версии против updated_at

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

Подход 1: колонка version (увеличивающийся целочисленный счетчик)

Добавьте поле version (обычно integer). Когда вы загружаете форму редактирования, вы также получаете текущую version. При сохранении вы отправляете это же значение обратно.

Обновление проходит только если сохранённая версия всё ещё совпадает с той, с которой пользователь начинал. Если совпадает — обновляете запись и увеличиваете version на 1. Если не совпадает — возвращаете конфликт вместо перезаписи.

Это легко понять: версия 12 значит «это 12‑е изменение». Такой подход избегает проблем, связанных со временем.

Подход 2: updated_at (сравнение меток времени)

Большинство таблиц уже имеют поле updated_at. Идея та же: читать updated_at при открытии формы и отправлять его при сохранении. Сервер обновляет запись только если updated_at не изменился.

Это может работать хорошо, но у меток времени есть подводные камни. Разные БД хранят с разной точностью. Некоторые округляют до секунды и тогда быстрые правки могут «выпасть». Если несколько систем пишут в одну базу, рассинхронивание часов и обработка часовых поясов тоже могут создавать запутанные кейсы.

Простое сравнение:

  • колонка версии: самое предсказуемое поведение, переносимо между БД, без проблем с часами;
  • updated_at: часто «бесплатно», потому что уже есть, но точность и обработка времени могут подвести.

Для большинства команд колонка версии — лучший основной сигнал. Она явная, предсказуемая и удобна для логов и тикетов поддержки.

Если вы строите в AppMaster, это обычно означает добавление целочисленного поля version в Data Designer и обеспечение проверки при обновлении. updated_at можно оставить для аудита, но пусть решение о безопасности правки принимает номер версии.

Что хранить и что отправлять при каждой правке

Оптимистичная блокировка работает только если каждая правка несёт маркер «последний увиденный» от момента открытия формы. Этот маркер может быть version или updated_at. Без него сервер не сможет понять, изменилась запись во время набора.

На самой записи храните обычные бизнес-поля плюс одно поле конкурентности, которым управляет сервер. Минимальный набор:

  • id (идентификатор)
  • бизнес-поля (name, status, price, notes и т. п.)
  • version (integer, увеличивается при каждом успешном обновлении) или updated_at (метка времени, пишется сервером)

Когда экран редактирования загружается, форма должна сохранить последнее увиденное значение поля конкурентности. Пользователи не должны его менять — держите его как скрытое поле или в состоянии формы. Пример: API возвращает version: 12, и форма хранит 12 до нажатия Сохранить.

При нажатии Сохранить отправляйте изменения и маркер последнего просмотра. Проще всего включить это в тело запроса обновления: id, изменённые поля и expected_version (или expected_updated_at). В AppMaster относитесь к этому как к любому другому привязанному значению: загрузили с записью, не меняли, отправили обратно с запросом.

На сервере обновление должно быть условным. Обновляйте только если ожидаемое значение совпадает с текущим в базе. Не «сливайте» молча.

Ответ с конфликтом должен быть понятен и удобен для обработки в UI. Практичный ответ включает:

  • HTTP‑статус 409 Conflict
  • короткое сообщение вроде «Запись была обновлена кем‑то другим.»
  • текущее серверное значение (current_version или current_updated_at)
  • опционально — актуальную запись целиком (чтобы UI мог показать, что изменилось)

Пример: Сэм открыл запись клиента с версией 12. Прия сохранила изменение, и версия стала 13. Сэм позже нажимает Сохранить с expected_version: 12. Сервер возвращает 409 с текущей записью на версии 13. UI может предложить Сэму просмотреть последние значения вместо перезаписи правки Прии.

Шаг за шагом: реализация оптимистичной блокировки end‑to‑end

Остановите тихие перезаписи
Создавайте админ-инструменты с серверными проверками версии через визуальную бизнес-логику.
Попробовать AppMaster

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

1) Добавьте поле конкурентности

Выберите одно поле, которое меняется при каждой записи.

Выделенная целочисленная version проще всего для понимания. Начните с 1 и инкрементируйте при каждом обновлении. Если у вас уже есть корректно обновляемое поле updated_at, можно использовать и его, но убедитесь, что оно меняется при каждой записи (включая фоновые задачи).

2) Отдавайте это значение клиенту при чтении

Когда UI открывает экран редактирования, включите текущую version (или updated_at) в ответ. Сохраните её в состоянии формы как скрытое значение.

Думайте о ней как о квитанции: «я редактирую то, что я видел в последний раз».

3) Требуйте это значение при обновлении

При сохранении клиент отправляет отредактированные поля плюс последнее увиденное значение конкурентности.

На сервере сделайте обновление условным. В SQL это выглядит так:

UPDATE tickets
SET status = $1,
    version = version + 1
WHERE id = $2
  AND version = $3;

Если обновление затронуло 1 строку — сохранение прошло. Если 0 строк — кто‑то уже изменил запись.

4) Возвращайте новое значение после успеха

После успешного сохранения возвращайте обновлённую запись с новой version (или новым updated_at). Клиент должен заменить состояние формы на то, что вернул сервер. Это предотвращает «двойные сохранения» со старой версией.

5) Относитесь к конфликтам как к нормальному исходу

Когда условное обновление терпит неудачу, возвращайте понятный ответ о конфликте (часто HTTP 409), включающий:

  • текущую запись как она есть сейчас
  • изменения, которые пытался внести клиент (или достаточно данных, чтобы реконструировать их)
  • какие поля отличаются (если можете это вычислить)

В AppMaster это хорошо ложится на модель: поле в Data Designer, endpoint чтения, и Business Process, который выполняет условное обновление и ветвится в успех или конфликт.

UI‑паттерны, которые обрабатывают конфликты без раздражения пользователей

Сделайте конфликты нормальной дорогой
Используйте условные обновления в Business Processes, чтобы вернуть аккуратную обработку 409 Conflict.
Построить workflow

Оптимистичная блокировка — это только половина дела. Другая половина — что видит человек, когда его сохранение отклонено из‑за того, что кто‑то другой изменил запись.

Хороший UI при конфликтах преследует две цели: остановить тихие перезаписи и помочь пользователю быстро завершить задачу. При правильной реализации это воспринимается как полезная страховка, а не как блокада.

Паттерн 1: простой блокирующий диалог (самый быстрый)

Используйте, когда правки небольшие, и пользователи могут безопасно повторить их после перезагрузки.

Короткое и конкретное сообщение: «Запись изменилась, пока вы редактировали. Перезагрузите, чтобы увидеть актуальную версию.» Дайте два чётких действия:

  • Перезагрузить и продолжить (основное)
  • Скопировать мои изменения (опционально, но полезно)

«Скопировать мои изменения» может поместить несохранённые значения в буфер обмена или сохранить их в форме после перезагрузки, чтобы не приходилось всё вспоминать.

Подходит для одиночных полей, переключателей, смены статусов или коротких заметок. Лёгко реализуется в большинстве билдов, включая экраны на AppMaster.

Паттерн 2: «Просмотреть изменения» (для важных записей)

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

  • «Ваши правки» (что вы пытались сохранить)
  • «Актуальные значения» (то, что сейчас в базе)
  • «Что изменилось с момента открытия» (поля с конфликтом)

Сфокусируйтесь на различиях: не показывайте все поля, если конфликтует только три.

Для каждого конфликтного поля предложите простые варианты:

  • Сохранить моё
  • Принять ихнее
  • Слить (когда это имеет смысл, например теги или заметки)

После разрешения конфликтов сохраните снова, используя самую новую версию. Для длинного текста полезно показать diff (добавлено/удалено), чтобы принять быстрое решение.

Когда разрешать принудительную перезапись (и кто это может делать)

Иногда нужна принудительная перезапись, но её нужно сделать редкой и контролируемой. Если добавляете её, сделайте действие осознанным: потребуйте короткую причину, логируйте, кто это сделал, и ограничьте роли (админы, супервизоры).

Для обычных пользователей используйте «Просмотреть изменения» по умолчанию. Принудительная перезапись оправдана, когда пользователь — владелец записи, запись низкого риска или система корректирует плохие данные под наблюдением.

Пример сценария: двое коллег редактируют одну запись

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

Таймлайн (с оптимистичной блокировкой через version или updated_at):

  • 10:02 — Майя открыла Клиента #4821. В форме: Status = "Needs follow-up", Notes = "Called yesterday" и Version = 7.
  • 10:03 — Джордан открыл ту же запись, тоже увидел Version = 7.
  • 10:05 — Майя меняет Status на "Resolved" и добавляет заметку: "Issue fixed, confirmed by customer." Нажимает Сохранить.
  • 10:05 — Сервер обновляет запись, инкрементирует Version до 8 (или обновляет updated_at) и сохраняет аудиторную запись: кто что и когда изменил.
  • 10:09 — Джордан дописывает заметку: "Customer asked for a receipt" и нажимает Сохранить.

Без проверки конкурентности сохранение Джордана может незаметно перезаписать правки Майи, в зависимости от того, как построено обновление. С оптимистичной блокировкой сервер отклонит сохранение Джордана, потому что он отправляет Version = 7, а запись уже на Version = 8.

Джордан увидит понятное сообщение о конфликте. UI покажет, что случилось, и предложит безопасные следующие шаги:

  • Перезагрузить актуальную запись (отказаться от моих правок)
  • Применить мои изменения поверх актуальной записи (если возможно)
  • Просмотреть различия («Мои» vs «Актуальные») и выбрать, что сохранить

Простой экран может показать:

  • «Этого клиента обновила Майя в 10:05»
  • Поля, которые изменились (Status и Notes)
  • Превью несохранённой заметки Джордана, чтобы он мог её скопировать или повторно применить

Джордан выбирает «Просмотреть различия», оставляет Status = "Resolved" Майи и дописывает свою заметку к существующим. Он сохраняет снова с Version = 8, обновление проходит, и версия становится 9.

Конечное состояние: нет потери данных, ясно кто что изменял, и есть аккуратный аудит, где видны как изменение статуса Майи, так и обе заметки по отдельности. В AppMaster это соответствует одной проверке на обновление и небольшому диалогу разрешения конфликта в админ‑UI.

Частые ошибки, которые всё ещё приводят к потере данных

Выберите путь развертывания
Развертывайте в AppMaster Cloud, AWS, Azure, Google Cloud или экспортируйте исходники.
Развернуть приложение

Большинство багов с «оптимистичной блокировкой» связаны не с идеей, а с передачей между UI, API и базой. Если хотя бы один слой забудет правила, тихие перезаписи всё ещё возможны.

Классическая ошибка — собрать версию при загрузке экрана, но не отправить её при сохранении. Это часто происходит, когда форма переиспользуется и скрытое поле теряется, или когда клиентский API отправляет только «изменённые» поля.

Другой распространённый косяк — проверка только в браузере. Пользователь может увидеть предупреждение, но если сервер всё равно принимает обновление, другой клиент или повторный запрос может молча перезаписать данные. Сервер — последний и главный арбитр.

Шаблоны, которые больше всего приводят к потере данных:

  • отсутствие токена конкурентности в запросе сохранения (version, updated_at или ETag);
  • принятие обновлений без атомарного условия — фильтрация только по id вместо id + version;
  • использование updated_at с низкой точностью (например, секунды); два изменения в одну секунду могут выглядеть одинаково;
  • замена больших полей (notes, descriptions) или массивов (tags, line items) без показа различий;
  • трактовка любого конфликта как «просто повторить», что может заново применить устаревшие значения поверх новых.

Конкретный пример: два лид‑агента открывают одну запись клиента. Один добавляет длинную внутреннюю заметку, другой меняет статус и сохраняет. Если ваше сохранение перезаписывает весь полезный полезет (payload), изменение статуса может стереть заметку.

Когда случается конфликт, команды всё ещё теряют данные, если ответ API слишком пустой. Не ограничивайтесь «409 Conflict». Возвращайте достаточно информации:

  • текущую серверную версию (или updated_at)
  • последние серверные значения полей, задействованных в конфликте
  • ясный список полей, которые отличаются
  • кто и когда это изменил (если вы это отслеживаете)

Если вы реализуете это в AppMaster, придерживайтесь дисциплины: храните версию в UI‑состоянии, отправляйте её с обновлением и принудительно сравнивайте внутри бэкенд‑логики перед записью в PostgreSQL.

Быстрая проверка перед релизом

Избегайте технического долга позже
Создавайте API и бизнес-логику визуально, затем регенерируйте чистый код по мере изменения требований.
Собрать бэкенд

Перед выкладкой прогоните чек‑лист по кейсам, которые создают ситуацию «сохранилось нормально», но тихо перезаписало чужую работу.

Проверки данных и API

Убедитесь, что у записи есть токен конкурентности во всем потоке. Это может быть version или updated_at, но это поле должно считаться частью записи, а не опциональной метаинформацией.

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

В AppMaster проверьте, что поле в Data Designer существует (version или updated_at), и что ваш Business Process сравнивает его перед фактическим обновлением.

UI‑проверки

Конфликт безопасен только если следующий шаг очевиден.

Когда сервер отклоняет обновление, покажите понятное сообщение: «Запись изменилась с тех пор, как вы её открыли.» Затем предложите одно безопасное действие первым: перезагрузить последние данные. Если возможно, добавьте путь «перезагрузить и повторно применить», который сохранит несохранённый ввод пользователя и попытается автоматически его применить к обновлённой записи, чтобы мелкие правки не превращались в повторный набор.

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

Следующие шаги: добавьте блокировку в один рабочий поток и масштабируйте

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

Выберите поведение по умолчанию заранее — оно влияет на бэкенд‑логику и UI:

  • Блокировать и перезагрузить: остановить сохранение, перезагрузить актуальную запись и попросить пользователя повторно применить изменение.
  • Просмотреть и слить: показать «ваши изменения» vs «последние изменения» и дать пользователю выбрать.

Блок‑и‑перезагрузка проще в реализации и хорошо работает для коротких правок. Просмотр‑и‑слияние стоит делать для длинных или критичных записей.

Затем реализуйте и протестируйте один полный поток прежде чем расширять:

  • выберите экран и перечислите поля, которые чаще всего правят;
  • добавьте version (или updated_at) в payload формы и требуйте его при сохранении;
  • сделайте обновление условным в запросе к базе (обновлять только если версия совпадает);
  • спроектируйте сообщение о конфликте и следующее действие (перезагрузить, скопировать текст, открыть сравнение);
  • протестируйте в двух браузерах: сохраните в вкладке A, затем попробуйте сохранить устаревшие данные в вкладке B.

Добавьте лёгкий лог событий для конфликтов. Даже простое событие «произошёл конфликт» с типом записи, именем экрана и ролью пользователя поможет найти горячие точки.

Если вы строите админ‑инструменты с AppMaster (appmaster.io), основные куски ложатся естественно: модельируйте поле версии в Data Designer, применяйте условные обновления в Business Processes и добавьте простой диалог конфликта в UI‑билдер. Когда первый рабочий поток стабилен, повторяйте этот шаблон и держите UI разрешения конфликтов единообразным, чтобы пользователи выучили поведение один раз и доверяли ему повсюду.

Вопросы и ответы

Что такое «тихая перезапись» и почему она происходит?

Тихая перезапись происходит, когда два человека редактируют одну и ту же запись в разных вкладках или сессиях, и последнее сохранение заменяет предыдущие изменения без предупреждения. Самое опасное — оба пользователя видят сообщение об успешном сохранении, поэтому потерянные правки замечают только позже.

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

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

Почему не просто заблокировать запись, чтобы никто другой не мог её править?

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

Чем лучше использовать номер версии или updated_at для проверки конфликтов?

Колонка версии обычно проще и предсказуемее — она избегает проблем с точностью времени и сбоев часов. Проверка по updated_at тоже может работать, но её легко сломать при низкой точности метки времени или при рассинхронизации часов между системами.

Какие данные нужно передавать, чтобы оптимистичная блокировка работала?

Нужен серверный токен конкурентности на записи — обычно version (integer) или updated_at (timestamp). Клиент читает его при открытии формы, хранит неизменным во время редактирования и отправляет обратно при сохранении как «ожидаемое» значение.

Почему проверка версии должна выполняться на сервере, а не только в UI?

Потому что клиент нельзя считать надежным защитником общих данных. Сервер должен принудительно выполнить условное обновление вроде «обновить где id и version совпадают», иначе другой клиент, повторный запрос или фоновая задача всё равно могут тихо перезаписать изменения.

Что должен видеть пользователь при возникновении конфликта?

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

Что API должен возвращать при конфликте, чтобы UI мог восстановиться?

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

Какие самые распространённые ошибки всё ещё приводят к потере данных?

Частые ошибки: отсутствие токена конкурентности при сохранении, обновления, фильтруемые только по id вместо id + version, и проверки по метке времени с низкой точностью. Также рискованно полностью заменять большие поля (заметки, массивы), потому что тогда проще нечаянно стереть чужие правки.

Как реализовать это в AppMaster без написания кода?

В AppMaster добавьте поле version в Data Designer и включите его в запись, которую UI загружает в состояние формы. Затем реализуйте условное обновление в Business Process, чтобы запись перезаписывалась только когда ожидаемая версия совпадает, а ветка конфликта обрабатывалась в UI — например, перезагрузка или просмотр различий.

Легко начать
Создай что-то невероятное

Экспериментируйте с AppMaster с бесплатной подпиской.
Как только вы будете готовы, вы сможете выбрать подходящий платный план.

Попробовать AppMaster
Оптимистичная блокировка для админ‑инструментов: предотвращайте тихие перезаписи | AppMaster