В процессе разработки интеграций через HTTP-сервисы или Web-сервисы программисты часто сталкиваются с ситуацией, когда метод Записать() выбрасывает исключение: «Ошибка при вызове метода контекста (Записать): Операция не может быть выполнена из-за несоответствия версии или отсутствия записи базы данных (возможно, запись была изменена или удалена)!». Особенно часто это проявляется при попытке обновить статус обмена непосредственно в узле плана обмена или в реквизите справочника.
В данной статье мы подробно разберем, почему возникает эта ошибка, как работает механизм оптимистической блокировки в 1С и какие архитектурные подходы помогут навсегда избежать подобных проблем в высоконагруженных системах.
Для начала проанализируем ситуацию с точки зрения платформы. У каждого ссылочного объекта в базе данных (Справочники, Документы, Планы обмена и т.д.) есть скрытое системное поле — DataVersion (версия данных). Этот механизм необходим для реализации оптимистической блокировки.
Рассмотрим стандартный цикл изменения объекта в коде:
ПолучитьОбъект(). В этот момент в оперативную память считываются не только реквизиты, но и текущий номер версии (например, «Версия 1»).Записать(). Перед тем как зафиксировать изменения, платформа сравнивает версию объекта в памяти с версией, которая записана в таблице базы данных прямо сейчас.В контексте HTTP-сервисов эта проблема обостряется из-за параллельности. Несколько запросов могут прийти практически одновременно, прочитать один и тот же узел плана обмена и попытаться обновить в нем статус. Кто-то один успеет записать, остальные получат ошибку.
Давайте выясним, почему запись в ПланыОбмена или Справочники для фиксации технических статусов — это плохая практика:
Самый правильный архитектурный путь — вынос всех технических статусов, дат последнего обмена и логов в отдельный Регистр сведений. Запись в регистр сведений (особенно если он независимый) выполняется гораздо быстрее, не сбрасывает кэш объектов и не подвержена ошибке несоответствия версий, так как там используется механизм «Замещения» записи, а не модификации существующего объекта.
Проанализируем пример кода для записи статуса в регистр:
МенеджерЗаписи = РегистрыСведений.СтатусыОбмена.СоздатьМенеджерЗаписи();
МенеджерЗаписи.УзелИнтеграции = СсылкаНаУзел;
МенеджерЗаписи.ДатаСобытия = ТекущаяДата();
МенеджерЗаписи.Статус = Перечисления.СтатусыОбмена.Успешно;
МенеджерЗаписи.Сообщение = "Обмен завершен без ошибок";
МенеджерЗаписи.Записать(Истина); // Параметр Истина означает замещение существующей записи
Если вам нужно хранить историю, сделайте регистр периодическим или добавьте измерение ИдентификаторЗапроса. Это позволит избежать любых конфликтов при записи данных из параллельных потоков HTTP-сервиса.
Если архитектурно вы обязаны изменить именно сам объект (например, обновить номер принятого сообщения в плане обмена), необходимо использовать управляемые блокировки. Это заставит второй поток дождаться завершения транзакции первого потока, вместо того чтобы сразу «вылетать» с ошибкой версии.
Разберем, как правильно установить блокировку перед изменением узла:
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("ПланОбмена.СинхронизацияДанныхЧерезУниверсальныйФормат");
ЭлементБлокировки.УстановитьЗначение("Ссылка", УзелСсылка);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
НачатьТранзакцию();
Попытка
Блокировка.Заблокировать();
// Теперь мы уверены, что никто другой не меняет этот узел в данный момент
УзелОбъект = УзелСсылка.ПолучитьОбъект();
УзелОбъект.НомерПринятого = НовыйНомер;
УзелОбъект.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("Обмен.Ошибка", УровеньЖурналаРегистрации.Ошибка, , , ОписаниеОшибки());
ВызватьИсключение;
КонецПопытки;
Важно помнить, что метод Блокировка.Заблокировать() должен вызываться внутри транзакции. Только так вы обеспечите целостность данных при высокой нагрузке.
В исключительных случаях, когда блокировки использовать сложно, можно применить метод «грубой силы» — перечитывание объекта при возникновении ошибки версии. Рассмотрим этот алгоритм:
ПолучитьОбъект().Пример реализации:
УзелОбъект = УзелСсылка.ПолучитьОбъект();
УзелОбъект.Статус = "Выполняется";
Попытка
УзелОбъект.Записать();
Исключение
// Возможно, версия изменилась. Попробуем еще раз.
УзелОбъект = УзелСсылка.ПолучитьОбъект();
УзелОбъект.Статус = "Выполняется";
УзелОбъект.Записать();
КонецПопытки;
Внимание: Этот метод считается «костылем» и не рекомендуется для постоянного использования, так как он не решает проблему конкуренции, а лишь маскирует её, увеличивая нагрузку на сервер.
Для стабильной работы интеграций в 1С придерживайтесь следующих правил:
РегистрыСведений для логирования и хранения состояний обмена.БлокировкаДанных для критичных участков кода.Перенос данных в регистр сведений не только уберет ошибку записи, но и сделает вашу систему более производительной и отзывчивой для пользователей.