Как правильно и надежно получать остатки по регистрам 1С и решить проблему "скачущих" данных

Программист 1С v8.3 (Обычные формы) Бухгалтерский учет IT и автоматизация бизнеса
← На главную

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

В этой статье мы подробно разберем, почему возникает подобный эффект, как устроен механизм обратного расчета остатков на уровне СУБД, и какие системные решения существуют для обеспечения стопроцентной надежности получаемых данных.

Суть проблемы: почему остатки "скачут"?

Рассмотрим классическую ситуацию. На календаре 13 октября, в базе активно работают пользователи, проводя документы текущей датой. Граница рассчитанных итогов по регистру (бухгалтерии или накопления) установлена на 30 сентября. Нам необходимо программно получить остатки по счету 43 (или любому другому разрезу) на начало дня 2 октября.

Для этого мы пишем стандартный запрос к виртуальной таблице остатков:


ВЫБРАТЬ
    ХозрасчетныйОстатки.Организация,
    ХозрасчетныйОстатки.Счет КАК Счет,
    ХозрасчетныйОстатки.Субконто1 КАК Номенклатура,
    ХозрасчетныйОстатки.КоличествоОстаток КАК КоличествоОстаток,
    ХозрасчетныйОстатки.СуммаОстаток КАК СуммаОстаток
ИЗ
    РегистрБухгалтерии.Хозрасчетный.Остатки(&Период, Счет = &Счет, , ) КАК ХозрасчетныйОстатки

Запустив этот запрос несколько раз подряд с интервалом в пару секунд, мы с удивлением обнаруживаем, что сумма или количество остатка колеблется. Отклонения в результатах в точности соответствуют объемам проведения новых документов от текущего числа (13 октября), хотя мы запрашиваем данные на глубокое прошлое — на 2 октября.

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

Физический механизм расчета остатков "назад" в СУБД

Для того чтобы понять природу ошибки, нам нужно заглянуть под капот платформы «1С:Предприятие» и СУБД (например, MS SQL Server).

Когда запрашиваемая дата получения остатков (02.10) находится позже даты рассчитанных итогов (30.09), платформа не может взять готовую запись из таблицы итогов. Ей приходится вычислять остаток динамически. СУБД делает это путем обратного пересчета от таблицы так называемых «актуальных итогов» (хранящихся на «конец времен» — 01.11.3999 года).

Математически формула выглядит следующим образом:

[Остаток на 02.10] = [Итог на 31.12.3999] - [Движения с 02.10 по 31.12.3999]

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

  1. Шаг 1: В физическую таблицу движений регистра добавляются новые записи.
  2. Шаг 2: В таблице итогов обновляется значение актуального остатка на 3999 год.

Поскольку оба шага выполняются в одной транзакции СУБД, для внешнего наблюдателя они должны быть неделимы (принцип ACID). Однако в старых версиях платформы или при определенных настройках конфигурации чтение данных в запросах по умолчанию оптимизировано с использованием хинта СУБД NOLOCK (уровень изоляции транзакций READ UNCOMMITTED или «грязное чтение»).

Если наш читающий запрос выполняется ровно в ту миллисекунду, когда Шаг 1 завершен (запись в движениях уже появилась), а Шаг 2 еще не завершился (актуальный итог на 3999 год еще не обновлен), СУБД берет старое значение итога на 3999 год и вычитает из него новое (уже записанное) движение за текущую дату. В результате обратного расчета получается математически некорректный заниженный или завышенный остаток на 2 октября.

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

Влияние режима совместимости конфигурации

Проанализируем результаты практических тестов на платформе «1С:Предприятие 8.3» под управлением MS SQL Server. Поведение системы кардинально зависит от настроек режима совместимости конфигурации:

Решение 1: Активация режима версионирования строк (RCSI)

Наиболее надежным и современным системным решением является перевод конфигурации на режим совместимости с платформой 8.3 и использование механизма RCSI (Read Committed Snapshot Isolation).

При включении этого режима на уровне СУБД MS SQL Server платформа 1С перестает использовать грязное чтение NOLOCK. Вместо этого задействуется механизм версионирования строк в базе tempdb.

Рассмотрим, как это работает:

  1. Когда транзакция записи изменяет строки в таблице итогов, старые версии этих строк сохраняются в специальном хранилище версий.
  2. Любой читающий запрос, запущенный параллельно, мгновенно получает доступ к согласованной (старой) версии строки до тех пор, пока транзакция записи не зафиксирована (Commit).
  3. В результате мы получаем абсолютно согласованные остатки без каких-либо блокировок и задержек в работе пользователей.

Решение 2: Использование управляемых блокировок

Если ваша конфигурация вынужденно работает в режиме совместимости с 8.2 (например, сильно кастомизированная «1С:Управление производственным предприятием 1.3»), и включить RCSI невозможно, необходимо использовать механизм управляемых блокировок.

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

Разберем пример кода на языке 1С:


// Создаем новый объект блокировки данных
Блокировка = Новый БлокировкаДанных;

// Указываем пространство блокировки - наш регистр
ЭлементБлокировки = Блокировка.Добавить("РегистрБухгалтерии.Хозрасчетный");

// Устанавливаем фильтр по счету, чтобы не блокировать весь регистр целиком
ЭлементБлокировки.УстановитьЗначение("Счет", ПланСчетов.Хозрасчетный.ТоварыНаСкладах);

// Задаем режим блокировки - Исключительный (или Разделяемый в зависимости от задачи)
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;

// Активируем блокировку (обязательно должно выполняться внутри транзакции!)
Блокировка.Заблокировать();

// Теперь мы можем безопасно читать остатки запросом
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	ХозрасчетныйОстатки.КоличествоОстаток КАК КоличествоОстаток
    |ИЗ
    |	РегистрБухгалтерии.Хозрасчетный.Остатки(&Период, Счет = &Счет, , ) КАК ХозрасчетныйОстатки";
Запрос.УстановитьПараметр("Период", Новый Граница(КонецДня(РабочаяДата), ГраницаТипа.Включая));
Запрос.УстановитьПараметр("Счет", ПланСчетов.Хозрасчетный.ТоварыНаСкладах);

РезультатЗапроса = Запрос.Выполнить();

Установка управляемой блокировки заставит наш запрос ожидать завершения параллельных транзакций записи по указанному счету. Это исключает грязное чтение и гарантирует получение корректных данных.

Решение 3: Конструкция "ДЛЯ ИЗМЕНЕНИЯ" в автоматическом режиме блокировок

Если база данных работает в автоматическом режиме блокировок, то простые запросы также будут использовать NOLOCK и возвращать неверные остатки. Чтобы принудительно заставить СУБД наложить блокировку чтения и дождаться окончания записи параллельных транзакций, в текст запроса следует добавить конструкцию ДЛЯ ИЗМЕНЕНИЯ:


ВЫБРАТЬ
    ХозрасчетныйОстатки.КоличествоОстаток КАК КоличествоОстаток
ИЗ
    РегистрБухгалтерии.Хозрасчетный.Остатки(&Период, Счет = &Счет, , ) КАК ХозрасчетныйОстатки
ДЛЯ ИЗМЕНЕНИЯ

При выполнении этого запроса внутри транзакции СУБД применит блокировку UPDLOCK, что заблокирует считываемые данные до окончания транзакции и предотвратит искажение остатков. Обратите внимание, что этот метод может снизить общую параллельную производительность системы, поэтому применять его следует только для критически важных участков кода.

Решение 4: Регулярное обслуживание и смещение итогов

Вероятность поймать ошибку обратного расчета напрямую зависит от того, насколько далеко запрашиваемая дата находится от рассчитанных итогов регистра. Если итоги рассчитаны по 30.09, а мы просим остатки на 02.10, глубина пересчета назад составляет почти полные полмесяца транзакций.

Рекомендуется настроить регламентное задание, которое будет регулярно (например, каждую ночь) смещать период рассчитанных итогов на конец предыдущего дня. Это сократит до минимума массив записей, участвующих в вычитании, снизит нагрузку на дисковую подсистему СУБД и значительно уменьшит вероятность возникновения коллизий при чтении.

Резюме

Для надежного получения остатков по регистрам СУБД выполните следующие шаги:

  1. По возможности откажитесь от режимов совместимости с версией 8.2 и переведите базу данных на платформу 8.3 с использованием режима RCSI на SQL-сервере.
  2. Для алгоритмов проведения документов всегда используйте механизм управляемых блокировок в транзакции.
  3. Если база работает в автоматическом режиме блокировок, используйте конструкцию ДЛЯ ИЗМЕНЕНИЯ для защиты от грязного чтения.
  4. Своевременно выполняйте регламентный пересчет и сдвиг актуальной границы итогов ближе к текущей дате.
← На главную