Как реализовать гибкие блокировки и оптимизировать работу 1С 7.7 на SQL Server при 200+ пользователях

Программист 1С v7.7 Управленческий учет Торговля и дистрибуция
← На главную

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

В данной статье мы разберем продвинутый метод внедрения так называемых «гибких блокировок», который позволяет распараллелить процессы проведения документов и записи данных, минимизируя простои системы. Рассмотрим пошагово, как обойти ограничения платформы, используя возможности MS SQL Server.

Анализ проблемы штатных блокировок

Проанализируем стандартное поведение 1С 7.7 при проведении документа. Платформа открывает транзакцию и накладывает блокировки на таблицы в следующем порядке:

  1. Блокировка таблицы _1SJOURN для записи состояния документа.
  2. Блокировка _1SSYSTEM для обновления Точки Актуальности (ТА).
  3. Блокировка таблиц регистров (остатков и движений).

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

Шаг 1: Разблокировка системных таблиц

Первым делом необходимо «разлочить» основные таблицы: _1SJOURN, _1SSYSTEM, _1SUIDCTL, а также справочники и документы. Это делается путем изменения параметров работы компоненты 1С++ или другими программными методами, позволяющими избежать штатного TABLOCKX. Однако просто снять блокировки нельзя — мы рискуем получить дубликаты ID и нарушение логической целостности ТА.

Шаг 2: Обеспечение уникальности IDDOC и ID справочников

При параллельной работе нескольких сессий получение максимального ID (хранимки _1sp__1SJOURN_MaxID и _1sp__1SUIDCTL_GetMaxID) должно быть строго последовательным, но очень коротким по времени. Рассмотрим, как оптимизировать эти процедуры с помощью SQL-хинтов UPDLOCK, ROWLOCK и HOLDLOCK.

Поскольку эти хранимые процедуры вызываются в момент создания объекта (метод Новый()) и выполняются миллисекунды, блокировка на уровне строки не вызовет заметных задержек, но гарантирует отсутствие дублей. Мы модифицируем штатные процедуры получения ID так, чтобы они блокировали только конкретную строку с кодом типа объекта, а не всю таблицу целиком.

Шаг 3: Управление Точкой Актуальности (ТА) через триггеры

Самая сложная часть — обновление Точки Актуальности в _1SSYSTEM. Штатно 1С пытается выставить блокировку ДО выполнения кода ОбработкаПроведения, а обновляет ее ПОСЛЕ. Если мы уберем блокировку, то другие документы могут «вклиниться» и записать свои данные, из-за чего ТА может прыгнуть назад или возникнет ошибка «Существуют более ранние документы».

Для решения этой проблемы применим триггер типа INSTEAD OF UPDATE на таблицу _1SSYSTEM. Это позволит нам программно контролировать, куда и когда сдвигается ТА, не блокируя всю таблицу на время проведения документа. Посмотрим на пример реализации такого триггера:


TRIGGER _1SSYSTEM_Control
ON [dbo].[_1SSYSTEM]
INSTEAD OF UPDATE
AS
SET NOCOUNT ON
UPDATE [dbo].[_1SSYSTEM]
SET CURDATE = newVals.CURDATE
    , CURTIME = case 
        when coalesce(lastDoc.docTime, newVals.CURTIME) > newVals.Curtime 
        then coalesce(lastDoc.docTime, newVals.CURTIME) 
        else newVals.CURTIME 
      end
    , EVENTIDTA = case 
        when coalesce(lastDoc.docTime, newVals.CURTIME) >= newVals.Curtime 
             and coalesce(lastDoc.docID, newVals.EVENTIDTA) > newVals.EVENTIDTA
        then coalesce(lastDoc.docID, newVals.EVENTIDTA)
        else newVals.EVENTIDTA
      end
    -- остальные поля (DBSIGN, DBSETUUID и т.д.) просто переносим из inserted
FROM
  [dbo].[_1SSYSTEM] as sysTable
  cross join (select top 1 * from inserted) as newVals
  outer apply (
    select top 1
      cast(dbo.StrToID(SUBSTRING(DATE_TIME_IDDOC, 9, 6)) as int) as docTime
      , IDDOC as docID
    from [dbo].[_1SJOURN] with (nolock)
    where [DATE_TIME_IDDOC] between convert(char(8), newVals.[CURDATE], 112) 
          and (convert(char(8), newVals.[CURDATE], 112) + 'ZZZ')
          and [CLOSED]&1 = 1
    order by [DATE_TIME_IDDOC] desc
   ) as lastDoc

Этот триггер при любой попытке обновления _1SSYSTEM автоматически находит последний реально проведенный документ в журнале (используя NOLOCK для чтения) и устанавливает ТА на него. Таким образом, даже если документы проводятся параллельно, ТА всегда будет указывать на самый поздний из них.

Шаг 4: Использование программных блокировок (AppLock)

Чтобы избежать логических ошибок (например, когда два документа одновременно списывают один и тот же товар в минус), необходимо реализовать механизм софтовых блокировок. Вместо блокировки таблиц SQL мы используем системную процедуру sp_getapplock.

Рассмотрим алгоритм:

  1. В начале процедуры ОбработкаПроведения формируем список ресурсов, которые нужно заблокировать (например, ИД товара + ИД склада).
  2. Сортируем этот список в алфавитном порядке (это критически важно для предотвращения Deadlocks).
  3. Вызываем sp_getapplock для каждого ресурса.
  4. По завершении транзакции блокировки будут сняты автоматически SQL-сервером.

Такой подход позволяет двум пользователям одновременно проводить документы, если они не пересекаются по конкретным товарам.

Шаг 5: Оптимизация счетчика NETCHGCN

Выясним причину еще одного узкого места — таблицы 1SUSERS и поля NETCHGCN. Это счетчик изменений, который 1С инкрементирует при каждой записи данных. При 200 пользователях запись в эту таблицу становится серьезной преградой.

Проанализируем ситуацию: этот счетчик нужен только для уведомления других сеансов об обновлении списков (журналов). Если вы используете внешние компоненты, такие как 1CPP и класс ТабличноеПоле, штатные механизмы обновления списков вам не нужны. В этом случае можно использовать триггер, который будет игнорировать обновления этой таблицы или выполнять их значительно реже, снимая нагрузку с транзакций проведения.

Шаг 6: Финализация проведения

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


// Пример вызова в конце ОбработкаПроведения
Позиция = ПолучитьПозицию();
ДатаДок = Сред(Позиция, 2, 8);
ВремяДок = СокрЛП(Сред(Позиция, 10, 10));
ИдДок = _IdToStr(СокрЛП(Сред(Позиция, 20)));
ТекстЗапроса = "exec [dbo].[_1sp__1SSYSTEM_ToLastDoc] '"+ДатаДок+"', "+ВремяДок+", '"+ИдДок+"'";
ЗапросSQL.Выполнить(ТекстЗапроса);

Внутри хранимой процедуры _1sp__1SSYSTEM_ToLastDoc мы выполняем обычный UPDATE таблицы _1SSYSTEM, который перехватывается нашим триггером и корректно устанавливает Точку Актуальности на текущий документ или более поздний, если кто-то уже успел завершить транзакцию раньше нас.

Дополнительные рекомендации по настройке SQL

Рассмотрим важные параметры MS SQL Server, которые помогут закрепить результат:

Внедрение вышеописанных методов позволяет превратить «уставшую» систему на 1С 7.7 в высокопроизводительный комплекс, способный переваривать сотни документов в минуту без заметных ожиданий для пользователей. Главное — помнить, что гибкие блокировки накладывают повышенную ответственность на разработчика в части контроля логической целостности данных.

← На главную