В практике работы с платформой 1С:Предприятие 8.2 (и ранних версий 8.3) в режиме управляемых блокировок программисты часто сталкиваются с необъяснимым на первый взгляд явлением. Во время массового перепроведения документов текущей датой отчеты за прошлые периоды (например, ОСВ за прошлый год) начинают показывать неверные, постоянно меняющиеся данные. Как только проведение заканчивается, остатки «приходят в норму».
В этой статье мы разберем внутренние механизмы платформы и СУБД MS SQL Server, которые приводят к этой проблеме, проанализируем поведение виртуальных таблиц и выясним, как обеспечить согласованность данных в отчетах без ущерба для производительности.
Первое, что стоит понять: почему отчет за прошлый период вообще «видит» изменения, происходящие в текущем моменте? Казалось бы, если мы меняем документ от 15.03.2024, это никак не должно влиять на остатки на 31.12.2023. Однако в 1С расчет остатков работает сложнее, чем кажется.
Рассмотрим механизм расчета остатков «от обратного». Платформа 1С хранит рассчитанные итоги на конец каждого месяца. Когда мы строим отчет на определенную дату, система выбирает наиболее эффективный путь:
Если в момент формирования отчета идет транзакция перепроведения документа, система может столкнуться с ситуацией, когда старые движения документа уже удалены из базы, а новые еще не зафиксированы или находятся в процессе записи. Если отчет читает данные без должной изоляции, он видит «пустое место» там, где должны быть обороты текущего периода. При расчете «назад» от текущих (искаженных в транзакции) итогов, остатки за прошлый период также оказываются неверными. Это и есть то самое «мерцание» данных.
Использование режима Разделение итогов в регистрах бухгалтерии или накопления призвано ускорить запись, позволяя нескольким пользователям одновременно проводить документы. В этом режиме данные хранятся не только в основной таблице итогов, но и в специальных таблицах разделителей (с суффиксом _Split).
В платформе 8.2 существовала архитектурная особенность: при выполнении запроса к виртуальной таблице остатков система объединяет данные из основной таблицы и таблиц разделителей. Если в этот момент выполняется интенсивная запись, алгоритм объединения в версиях до 8.3.5 мог выдавать промежуточный результат, не учитывающий данные, заблокированные пишущей транзакцией. Это подтверждается официальными списками исправлений платформы, где проблема несогласованного чтения при включенном разделении итогов была окончательно решена только в версии 8.3.5.1248.
Еще одна причина кроется в настройках MS SQL Server. По умолчанию 1С часто работает в режиме Read Uncommitted для операций чтения в отчетах, чтобы не блокировать работу пользователей. Если в коде проведения документа используется явная запись набора записей, данные попадают в SQL-таблицы еще до завершения транзакции (COMMIT).
Разберем пример кода в ОбработкаПроведения, который провоцирует проблему:
Процедура ОбработкаПроведения(Отказ, Режим)
// 1. Очищаем старые движения
Для Каждого Набор Из Движения Цикл
Набор.Очистить();
Набор.Записывать = Истина;
КонецЦикла;
// 2. Физически записываем пустые наборы в БД (начало "грязного окна")
Движения.Записать();
// 3. Выполняем сложные расчеты
ТаблицаДвижений = СформироватьДвиженияПоРасчетам();
// 4. Заполняем наборы новыми данными
ДобавитьПроводкиИзТаблицы(ТаблицаДвижений);
// 5. Записываем окончательные данные
Движения.Записать();
КонецПроцедуры
В момент между первой и второй записью (пункты 2 и 5) в базе данных по данному документу движений нет. Любой отчет, запущенный параллельно, увидит отсутствие этих проводок и выдаст неверный остаток, даже если он формируется за период столетней давности (из-за механизма расчета «от обратного»).
Рассмотрим несколько способов устранения этой ошибки, от самых простых до радикальных.
Это наиболее правильный технический путь при использовании MS SQL Server. Суть метода заключается в том, что сервер базы данных начинает хранить версии строк. Когда отчет пытается прочитать данные, которые сейчас меняются другим пользователем, SQL Server подсовывает отчету последнюю согласованную версию этих данных до начала транзакции.
Для включения этого режима необходимо выполнить SQL-запрос (потребуются права администратора БД):
ALTER DATABASE [ИмяВашейБазы] SET ALLOW_SNAPSHOT_ISOLATION ON;
ALTER DATABASE [ИмяВашейБазы] SET READ_COMMITTED_SNAPSHOT ON;
Важно: Перед выполнением запроса необходимо отключить всех пользователей от базы данных. Этот режим значительно снижает вероятность блокировок «читатель-писатель» и решает проблему грязного чтения.
Выясним, на какую дату рассчитаны итоги в вашей системе. Если итоги рассчитаны на дату, сильно отстоящую от текущей, система вынуждена пересчитывать огромный объем движений. Убедитесь, что регламентное задание по расчету итогов выполняется регулярно и итоги актуальны на конец текущего или предыдущего месяца.
Используйте метод УстановитьПериодРассчитанныхИтогов() или стандартную обработку «Управление итогами», чтобы зафиксировать актуальное состояние регистров.
Проанализируем код проведения. Чтобы минимизировать время, когда данные в базе «искажены», старайтесь избегать многократной записи движений внутри одной транзакции. Вместо очистки и записи «пустого» набора в начале, выполняйте все расчеты в памяти, заполняйте коллекцию Движения и записывайте её один раз в самом конце процедуры ОбработкаПроведения.
Также стоит помнить, что свойство БлокироватьДляИзменения для набора записей в режиме управляемых блокировок работает специфично:
// Это свойство устанавливает управляемую блокировку при записи,
// но оно не спасет от чтения "грязных" данных другими отчетами,
// если те не используют транзакцию с уровнем изоляции Repeatable Read.
Движения.РегистрБухгалтерии.БлокироватьДляИзменения = Истина;
Если вы все еще используете 1С 8.2, рассмотрите возможность перехода на актуальные релизы 8.3. Как мы выяснили выше, в платформе была проведена серьезная работа над оптимизацией виртуальных таблиц и механизмов работы с разделителями итогов. В новых версиях 1С гораздо лучше обрабатывает ситуации параллельного чтения и записи, минимизируя влияние транзакций на расчет остатков в прошлых периодах.
Проблема влияния текущих документов на прошлые остатки — это не «баг» конкретного отчета, а следствие комбинации факторов: механизма оптимизации расчета итогов 1С, настроек изоляции транзакций SQL Server и особенностей работы разделения итогов. Наилучшим решением является включение режима RCSI в SQL Server и обновление платформы до версии не ниже 8.3.5, что позволит отчетам всегда видеть согласованную картину мира.