При работе в высоконагруженных системах на базе 1С:Предприятие 8.2 и 8.3 программисты часто сталкиваются с ситуацией, когда два процесса, рассчитывающие разные документы (например, «Начисление зарплаты»), блокируют друг друга. Самое странное в этой ситуации то, что наборы записей абсолютно не пересекаются ни по измерениям, ни по регистратору, а режим управления блокировками установлен в значение Управляемый. Казалось бы, 1С должна эффективно разводить такие транзакции, но на практике возникают таймауты на уровне СУБД.
В этой статье мы подробно разберем, почему возникают блокировки на уровне Microsoft SQL Server, как на это влияет таблица фактического периода действия и какие настройки СУБД помогут нам распараллелить процессы без ошибок.
Прежде всего, проанализируем ситуацию, в которой оказывается разработчик. При параллельном расчете нескольких крупных документов (например, по 4000 строк в каждом) происходит запись в регистр расчета, например, ОсновныеНачисленияРаботниковОрганизаций. Мы используем стандартный метод Записать(). Рассмотрим типичный код, вызывающий проблему:
Набор = РегистрыРасчета.ОсновныеНачисленияРаботниковОрганизаций.СоздатьНаборЗаписей();
Набор.Отбор.Регистратор.Установить(Регистратор);
Набор.Очистить();
// Третий параметр - ПересчитатьФактическийПериодДействия
Набор.Записать(Истина, Ложь, Истина, Ложь);
Проблема кроется в том, что управляемые блокировки 1С работают на логическом уровне (блокируют конкретные значения измерений и регистратор), а блокировки СУБД работают на физическом уровне (блокируют строки, страницы или таблицы целиком). Когда мы записываем набор записей с расчетом фактического периода действия, система обращается к вспомогательной таблице _CRgActP (Actual Period). Именно здесь происходит основное «сражение» транзакций.
Выясним причину, по которой СУБД выбирает слишком грубые блокировки. Таблица _CRgActP используется для хранения пересечений периодов действия начислений. В отличие от основной таблицы регистра, она крайне чувствительна к массовым операциям удаления и вставки. Проанализируем типичный запрос DELETE, который формирует платформа при перезаписи набора:
DELETE FROM T2
FROM #tt196 T1 WITH(NOLOCK)
INNER JOIN _CRgActP561 T2 ON T2._RecorderTRef = T1._RecorderTRef
AND T2._RecorderRRef = T1._RecorderRRef
AND T2._LineNo = T1._LineNo
WHERE T2._RecorderTRef = T1._RecorderTRef
AND T2._RecorderRRef = T1._RecorderRRef
AND T2._LineNo = T1._LineNo
Обратим внимание на важный нюанс: платформа использует временную таблицу #tt196 для сопоставления записей. Если SQL Server видит, что объем удаляемых данных велик, или если статистика по таблице _CRgActP устарела, он может выполнить Index Scan вместо Index Seek. Это означает, что СУБД начнет сканировать индекс целиком, накладывая блокировки на страницы данных, которые ей даже не принадлежат.
Посмотрим на результаты трассировки SQL Profiler. Часто мы видим тип блокировки PAGE и режим IU (Intent Update). Что это означает для нас? Данные в SQL Server хранятся страницами по 8 КБ. Даже если наши регистраторы разные, физически их записи могут оказаться «соседями» на одной и той же странице. Когда одна транзакция пытается изменить свою строку на странице, она вешает намерение на обновление (IU) на всю страницу. Вторая транзакция, работая со своими записями на этой же странице, сталкивается с ожиданием.
Ситуация усугубляется, если происходит эскалация блокировок — переход от блокировки отдельных строк к блокировке целых страниц или всей таблицы. Хотя флаги трассировки 1211 или 1224 могут помочь предотвратить эскалацию до уровня таблицы, они не решают проблему конкуренции за одну страницу (PAGE).
Одним из самых эффективных способов борьбы со страничными блокировками является изменение коэффициента заполнения страниц (Fill Factor). Рассмотрим подробнее, как это работает. По умолчанию SQL Server заполняет страницы максимально плотно. Если мы установим Fill Factor на уровне 70-80%, система будет оставлять свободное место на страницах. Это значительно снижает вероятность того, что записи двух разных регистраторов попадут на одну страницу, и предотвращает «расщепление страниц» (Page Splits) при интенсивной записи.
Для реализации этого решения выполним следующие шаги:
_CRgActP561) через консоль запросов или стандартные средства СУБД.FILLFACTOR = 80.PAGE.Проанализируем поведение оптимизатора запросов. Если статистика по таблице фактических периодов не актуальна, SQL Server не может правильно оценить стоимость доступа к данным и выбирает сканирование индекса (Index Scan). При массовом расчете зарплаты данные меняются молниеносно, и статистика, собранная ночью, уже к обеду становится бесполезной.
Рекомендуем настроить регламентное задание на обновление статистики (UPDATE STATISTICS) именно для проблемных таблиц регистров расчета с высокой периодичностью (например, раз в час в период активного расчета). Для версий SQL Server ниже 2016 также полезно включить флаг трассировки 2371, который делает порог обновления статистики динамическим (чем больше таблица, тем меньший процент изменений требуется для обновления).
Рассмотрим вариант, когда мы можем изменить логику записи. Метод Записать() имеет четвертый параметр — ПересчитатьФактическийПериодДействия. Если мы передаем туда Истина, платформа в этой же транзакции выполняет тяжелые запросы к таблице _CRgActP.
Посмотрим на альтернативную стратегию:
Ложь (не рассчитывать фактический период сразу). Это позволит быстро зафиксировать движения в основной таблице регистра без блокировок в _CRgActP.Выясним, какие настройки сервера влияют на параллелизм. Параметр MAXDOP (Max Degree of Parallelism) играет ключевую роль. Для баз данных 1С часто рекомендуется устанавливать MAXDOP = 1. Это гарантирует, что один запрос не захватит все ядра процессора и не раздует пространство блокировок до невероятных масштабов, что часто случается при параллельном выполнении запросов на изменение данных.
Также проверим настройку LOCK_ESCALATION для конкретной таблицы. В современных версиях SQL Server (начиная с 2008 R2) можно отключить эскалацию до уровня таблицы командой:
ALTER TABLE _CRgActP561 SET (LOCK_ESCALATION = DISABLE);
Это заставит СУБД использовать только блокировки строк или страниц, не блокируя весь регистр целиком даже при очень больших объемах данных.
Конфликт блокировок при записи непересекающихся наборов в регистрах расчета — это не ошибка в вашем коде, а следствие особенностей физического хранения данных. Для решения проблемы мы рекомендуем комплексный подход:
FILLFACTOR для разрежения данных на страницах.Index Scan.Записать().Применяя эти методы, мы сможем добиться стабильной параллельной работы пользователей даже в самых нагруженных базах ЗУП и ЗБУ.