Как предотвратить задвоения заказов на перемещение в 1С, используя управляемые блокировки и управляя эскалацией в MS SQL Server?

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

При работе высоконагруженных систем 1С, особенно с параллельно выполняющимися регламентными заданиями и подписками на события, мы часто сталкиваемся с проблемой конкурентного доступа к данным. Один из распространённых сценариев — задвоение документов, например, заказов на перемещение, когда несколько процессов одновременно пытаются создать их по одним и тем же потребностям. Если наш регламент отрабатывает не очень быстро, а алгоритм не оптимален, это может приводить к коллизиям и нарушению целостности данных.

Для решения этой проблемы, вместо "колхозных" признаков выполнения, правильнее использовать механизм управляемых блокировок в 1С:Предприятии. Рассмотрим, как это сделать, и какие нюансы могут возникнуть при взаимодействии с MS SQL Server.

Введение в управляемые блокировки 1С

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

  1. Контроль со стороны прикладного кода: Мы самостоятельно определяем, какие данные и в каком режиме необходимо блокировать. Это позволяет учитывать специфику бизнес-логики нашего приложения и гибко настраивать блокировки.
  2. Гранулярность: В отличие от автоматических блокировок, которые могут укрупняться до уровня страниц или таблиц, управляемые блокировки позволяют нам блокировать необходимые области данных гораздо точнее. Мы можем блокировать отдельные записи регистров, что значительно увеличивает параллельность работы пользователей и процессов.
  3. Работа в рамках транзакции: Управляемые блокировки устанавливаются и снимаются только в рамках транзакции. Их время жизни равно времени жизни транзакции – они снимаются автоматически при фиксации или откате транзакции. Это означает, что блокировку следует устанавливать как можно ближе к началу использования блокируемых данных и минимизировать время удержания, завершая транзакцию как можно быстрее.
  4. Независимость от СУБД: Механизм управляемых блокировок работает единообразно для всех систем управления базами данных, поддерживаемых платформой 1С, будь то MS SQL Server, PostgreSQL или другие.
  5. Использование менеджера блокировок 1С: Система 1С применяет собственный менеджер блокировок, который выполняет дополнительную проверку на соответствие установленным ограничениям, прежде чем передать запрос на блокировку в СУБД.

Чтобы применить управляемые блокировки, мы будем использовать объект МенеджерБлокировок. Давайте посмотрим на пример его использования для блокировки записей регистра, по которому читаются потребности:


// Создаем экземпляр менеджера блокировок
МенеджерБлокировок = Новый МенеджерБлокировок;

// Добавляем объект блокировки для регистра накопления "Потребности"
// Важно указать полное имя объекта метаданных
БлокировкаДанных = МенеджерБлокировок.Добавить("РегистрНакопления.Потребности");

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

// Устанавливаем значения измерений, по которым мы хотим заблокировать записи.
// Это позволяет заблокировать только конкретные строки, а не весь регистр.
// Мы должны указать все измерения, которые однозначно идентифицируют обрабатываемые потребности.
// Например, если потребности определяются складом и номенклатурой:
БлокировкаДанных.УстановитьЗначение("Склад", СкладПолучатель); 
БлокировкаДанных.УстановитьЗначение("Номенклатура", Номенклатура);
// Если в выборку попадает несколько номенклатур для одного склада,
// потребуется добавить несколько объектов БлокировкаДанных,
// или блокировать по менее детализированным измерениям, если это приемлемо.
// В идеале, мы блокируем те записи, которые однозначно определяются нашим запросом потребностей.

// После настройки всех необходимых объектов блокировки, применяем их.
// Это должно быть сделано в начале транзакции, до чтения или изменения данных.
Попытка
    МенеджерБлокировок.Заблокировать();
    // Начало транзакции должно быть до вызова Заблокировать(),
    // или платформа автоматически начнет транзакцию при первой записи.
    НачатьТранзакцию();

    // Теперь, когда данные заблокированы, мы можем безопасно читать потребности
    // и создавать заказы на перемещение.
    // Запрос к регистру "Потребности" будет ожидать снятия блокировки,
    // если другие процессы уже заблокировали эти же записи.
    // ... Выполняем наш "не очень хороший алгоритм", который читает потребности
    //     и создает заказы. ...

    // После успешного создания заказов и записи всех изменений, фиксируем транзакцию.
    // Блокировки будут автоматически сняты.
    ЗафиксироватьТранзакцию();
Исключение
    // В случае ошибки, отменяем транзакцию, и блокировки также будут сняты.
    ОтменитьТранзакцию();
    ВызватьИсключение; // Передаем исключение дальше по стеку
КонецПопытки;

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

Эскалация блокировок в MS SQL Server и её влияние на производительность

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

Что такое эскалация блокировок: Это процесс, при котором SQL Server автоматически преобразует большое количество мелких блокировок (например, на уровне строк или страниц) в одну более крупную блокировку (например, на уровне таблицы). Этот механизм является оптимизацией, направленной на снижение накладных расходов на управление блокировками и уменьшение потребления памяти сервером SQL. Меньше блокировок означает меньше ресурсов для их управления.

Причины и последствия эскалации:

  1. Порог эскалации: SQL Server динамически определяет, когда выполнять эскалацию блокировки. Это основано на нескольких факторах: количестве удерживаемых блокировок (часто упоминается порог в 5000 блокировок в рамках одной транзакции), количестве блокировок в рамках транзакции и общем объеме памяти, используемой для блокировок в системе (например, 24% от памяти, используемой ядром СУБД, если опция `locks` установлена в 0).
  2. Влияние на производительность: Хотя эскалация блокировок может быть полезна для снижения нагрузки на менеджер блокировок SQL Server, она часто приводит к снижению параллелизма и усилению блокировок. Если из-за эскалации блокируется вся таблица, а не только отдельные записи, это может вызвать проблемы с таймаутами и взаимоблокировками для других запросов, работающих с этой же таблицей. То есть, несмотря на то, что мы в 1С устанавливаем гранулярные блокировки на строки, SQL Server может их "укрупнить" до таблицы, сводя на нет наши усилия.
  3. Распространенные сценарии: Эскалация часто происходит при массовом изменении данных в таблице (например, при выполнении операций INSERT, UPDATE, DELETE), а также при выполнении запросов, которые не могут эффективно использовать индексы и вынуждены сканировать всю таблицу, приобретая большое количество блокировок.

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

Методы борьбы с эскалацией блокировок

Мы выяснили, что эскалация блокировок может стать проблемой. Разберем, как мы можем предотвратить нежелательную эскалацию, которая потенциально может "повесить" продуктивную систему.

  1. Трассировочный флаг -t1211 (Trace Flag 1211)

    Этот флаг является глобальной настройкой и полностью отключает эскалацию блокировок для всего экземпляра SQL Server.

    Действие: При включении флага -t1211 SQL Server перестает укрупнять блокировки. Он предотвращает эскалацию как по достижении предельного количества блокировок, так и по достижении предельного размера памяти, занятой для хранения информации о них.

    Потенциальные риски: Использование этого флага может привести к значительному росту потребления памяти сервером SQL для хранения информации о большом количестве мелких блокировок. Если памяти, выделяемой для структур блокировок, не хватает, могут возникать ошибки 1204 ("SQL Server не может получить ресурс LOCK в данный момент"). Поэтому его применение должно быть тщательно протестировано и обосновано. Мы не рекомендуем его использовать без глубокого понимания последствий и только после консультации с опытным DBA.

    Пример включения и отключения трассировочного флага (требует перезапуска SQL Server для постоянного применения, либо работает до перезапуска):

    
    -- Пример включения трассировочного флага для текущего сеанса
    DBCC TRACEON (1211);
    -- Пример включения трассировочного флага для всех сеансов (глобально)
    -- Изменения вступят в силу немедленно, но для сохранения после перезапуска
    -- SQL Server, флаг нужно добавить в параметры запуска службы.
    DBCC TRACEON (1211, -1);
            
    -- Пример отключения
    DBCC TRACEOFF (1211, -1);
    
  2. Опция LOCK_ESCALATION = DISABLE для конкретной таблицы

    Эта опция позволяет нам точечно отключить эскалацию блокировок для одной конкретной таблицы, а не для всего экземпляра SQL Server.

    Действие: При установке LOCK_ESCALATION = DISABLE SQL Server не будет эскалировать блокировки до уровня таблицы для этой конкретной таблицы, независимо от количества удерживаемых блокировок. Он будет продолжать работать с блокировками на уровне строк или страниц.

    Преимущества: Это более локальное и целенаправленное решение проблемы с эскалацией блокировок для конкретной "проблемной" таблицы, сохраняя при этом автоматическое управление блокировками для остальных таблиц. Изменения, внесенные с помощью ALTER TABLE ... SET (LOCK_ESCALATION = DISABLE), являются постоянными до тех пор, пока мы их не отменим вручную.

    Риски: Как и в случае с флагом -t1211, отключение эскалации для таблицы может привести к увеличению потребления памяти и потенциально к проблемам с производительностью из-за огромного количества мелких блокировок, если таблица подвергается очень интенсивным операциям. Рекомендуется применять только после тщательного анализа и тестирования в нерабочей среде.

    Пример отключения эскалации для таблицы:

    
    USE [ИмяБазы]; -- Укажите имя вашей базы данных
    -- _ReferenceChngR12526 - это пример имени таблицы.
    -- Нам нужно найти физическое имя таблицы регистра "Потребности" в SQL Server.
    ALTER TABLE _ReferenceChngR12526 SET (LOCK_ESCALATION = DISABLE);
    

    Для контроля, какие таблицы имеют отключенную эскалацию, мы можем использовать следующий запрос:

    
    USE [ИмяБазы]; -- Укажите имя вашей базы данных
    SELECT lock_escalation, lock_escalation_desc, name
    FROM sys.tables
    WHERE lock_escalation_desc='DISABLE';
    

    Предостережение: Мы не можем просто так "запилить" это в тиражное решение, особенно Совместимое. Изменения на уровне СУБД, такие как LOCK_ESCALATION = DISABLE, должны быть частью хорошо продуманной стратегии администрирования базы данных и применяться только после тщательного тестирования и анализа производительности.

Общие стратегии и лучшие практики

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

  1. Оптимизация запросов и транзакций: Всегда стремимся оптимизировать запросы. Избегаем использования функций в условиях WHERE (так называемые non-SARGable-условия), которые могут приводить к полному сканированию таблицы и, как следствие, к большому количеству блокировок. Стараемся разбивать большие транзакции на более мелкие, чтобы уменьшить время удержания блокировок. Чем короче транзакция, тем быстрее освобождаются ресурсы.
  2. Использование подсказок блокировок (Locking Hints): В некоторых случаях, когда все остальные методы исчерпаны, можно явно указать подсказки блокировок, такие как WITH (ROWLOCK), чтобы принудительно использовать блокировки на уровне строк и предотвратить эскалацию. Однако это также требует осторожности и глубокого понимания последствий, так как может усложнить управление блокировками SQL Server.
  3. Мониторинг: Регулярный мониторинг блокировок и взаимоблокировок является ключом к выявлению проблем. Используем расширенные события (Extended Events) SQL Server для отслеживания события lock_escalation и других событий, связанных с блокировками. Это поможет нам точно определить, когда и почему происходят проблемы. Также полезно отслеживать метрики по количеству таймаутов и дедлоков в системах мониторинга (например, Zabbix), чтобы оперативно реагировать на возникающие проблемы. Нулевое количество дедлоков в рабочее время - это норма.
  4. Изоляция транзакций: Платформа 1С для MS SQL Server использует уровень изоляции Read Committed Snapshot, а для PostgreSQL — Read Committed. Эти уровни изоляции в большинстве случаев обеспечивают высокую параллельность. Однако в некоторых ситуациях для обеспечения абсолютной целостности данных требуются явные управляемые блокировки.
  5. Выявление "говнокода": Зачастую проблемы с блокировками вызваны неоптимальным кодом в конфигурации 1С, который приводит к длительным транзакциям или избыточному объему данных для обработки. Исправление такого кода является приоритетной задачей. Часто "лечение флагами" лишь откладывает решение основной проблемы.
  6. Осторожное применение глобальных настроек: Изменение глобальных параметров сервера SQL, таких как трассировочные флаги, или настроек на уровне таблиц (например, LOCK_ESCALATION = DISABLE), должно осуществляться только опытными администраторами баз данных после тщательного тестирования, поскольку это может иметь серьезные последствия для производительности и стабильности системы.

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

← На главную