В процессе разработки механизмов интеграции программисты часто сталкиваются с ситуацией, когда данные «улетают» во внешнюю систему раньше, чем основная транзакция записи документа была успешно завершена. Представьте сценарий: документ ПеремещениеТоваров регистрируется в плане обмена WMS, но из-за ошибки в коде или сбоя оборудования транзакция записи документа откатывается. Однако за те миллисекунды, что прошли между регистрацией и откатом, регламентное задание обмена успевает прочитать запись из таблицы изменений и отправить её в стороннюю систему. В итоге в базе-приемнике данные есть, а в базе-источнике — нет. Проанализируем, почему так происходит и как это исправить.
Основная причина кроется в особенностях работы уровней изоляции транзакций в SQL Server и того, как платформа 1С взаимодействует с СУБД. Проанализируем ситуацию: когда мы выполняем чтение данных запросом вне контекста транзакции, платформа 1С (особенно в клиент-серверном варианте) часто использует уровень изоляции READ UNCOMMITTED. Это позволяет избежать взаимных блокировок и повышает производительность, но порождает эффект «грязного чтения» — сеанс чтения видит данные, которые еще не зафиксированы (не подтверждены COMMIT) другим сеансом.
Важно понимать природу управляемых блокировок. В отличие от жестких блокировок СУБД, управляемые блокировки 1С являются кооперативными. Это означает, что менеджер блокировок проверяет наличие конфликта только в том случае, если оба сеанса (и пишущий, и читающий) явно обратились к объекту БлокировкаДанных. Если процесс записи установил блокировку, а процесс чтения просто выполнил запрос к таблице без установки блокировки, он «не заметит» преграду и прочитает незавершенные данные.
Первое и самое важное правило для обеспечения целостности обмена: процесс чтения данных из плана обмена (выгрузка) должен обязательно происходить внутри транзакции. Посмотрим на пример того, как должен выглядеть правильный алгоритм выгрузки:
НачатьТранзакцию();
Попытка
// Устанавливаем управляемую блокировку, чтобы дождаться завершения других транзакций
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("ПланОбмена.WMS");
ЭлементБлокировки.УстановитьЗначение("Узел", УзелОбменаВМС);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Разделяемый;
Блокировка.Заблокировать();
// Теперь читаем изменения - мы увидим только зафиксированные данные
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ ... ИЗ ПланОбмена.WMS.Изменения(&Узел)";
Запрос.УстановитьПараметр("Узел", УзелОбменаВМС);
Результат = Запрос.Выполнить();
// Выполняем логику выгрузки...
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписатьЛогСобытий(ОписаниеОшибки());
КонецПопытки;
Когда мы вызываем НачатьТранзакцию(), уровень изоляции повышается до READ COMMITTED (или используется механизм RCSI), что гарантирует: запрос не увидит строки, которые находятся в процессе записи в другом сеансе, пока те не будут подтверждены.
Чтобы процесс выгрузки «увидел» блокировку, установленную при записи документа, необходимо корректно настроить её в модуле объекта документа (обычно в процедуре ПриЗаписи). Рассмотрим, как правильно установить исключительную блокировку на узел плана обмена:
Процедура ПриЗаписи(Отказ)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Устанавливаем исключительную блокировку на область изменений конкретного узла
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("ПланОбмена.WMS");
ЭлементБлокировки.УстановитьЗначение("Узел", УзелОбменаВМС);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
// Регистрируем изменения
ПланыОбмена.ЗарегистрироватьИзменения(УзелОбменаВМС, ЭтотОбъект.Ссылка);
КонецПроцедуры
Важный нюанс: в коде автора темы блокировка устанавливалась на всю область ПланОбмена.WMS без указания конкретного узла. Хотя это допустимо, лучше всегда уточнять значение узла через УстановитьЗначение("Узел", ...), чтобы не блокировать регистрацию изменений для других независимых узлов обмена.
Проанализируем ситуацию с использованием прямых запросов к виртуальным таблицам .Изменения. Использование метода ПланыОбмена.ВыбратьИзменения() является более безопасным с точки зрения платформенных механизмов. Этот метод внутри себя уже содержит необходимые оптимизации по работе с блокировками и гарантирует, что вы получите целостный набор данных.
Если вы все же используете запросы (например, для сложной фильтрации), помните о советах из Сообщения 5: выполняйте чтение малыми порциями. Длительная транзакция на чтение с установленной управляемой блокировкой может «подвесить» запись новых документов другими пользователями, так как они не смогут установить свою исключительную блокировку в ПриЗаписи, пока ваша разделяемая блокировка на чтение не будет снята.
Для гарантированного устранения проблемы «фантомных» выгрузок необходимо соблюдать три условия:
БлокировкаДанных по одной и той же области (в данном случае — ПланОбмена.ИмяПлана).READ_COMMITTED_SNAPSHOT. Это позволит читателям не блокировать писателей (и наоборот) на уровне СУБД, перекладывая логику синхронизации на плечи менеджера управляемых блокировок 1С.Следуя этим правилам, мы обеспечим надежную работу обмена данными даже в высоконагруженных системах с частыми транзакциями и возможными откатами.