Как проверить, заблокирован ли регистр сведений на чтение в 1С, и как устранить конфликт блокировок

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

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

Типичный текст ошибки выглядит так:

Конфликт блокировок при выполнении транзакции: Microsoft SQL Server Native Client 10.0: Превышено время ожидания запроса на блокировку. HRESULT=80040E31, SQLSrvr: SQLSTATE=HYT00, state=38, Severity=10, native=1222, line=1

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

Можно ли проверить, заблокирован ли регистр на чтение перед запросом?

Начнем с главного вопроса, который часто задают начинающие разработчики: «Можно ли средствами 1С предварительно узнать, заблокирована ли таблица регистра на чтение?»

Короткий ответ — напрямую средствами встроенного языка 1С этого сделать нельзя.

В платформе 1С отсутствуют методы класса РегистрСведенийМенеджер вроде Заблокирован(). Существующие в объектной модели методы блокировки (например, метод Заблокирован() у объектов документов или справочников) относятся исключительно к оптимистическим (объектным) блокировкам в рамках конкретного сеанса. Они защищают элемент от изменения другим пользователем в интерфейсе, но не имеют никакого отношения к транзакционным блокировкам на уровне СУБД (MS SQL, PostgreSQL и др.).

Единственный условный способ «проверить» блокировку в коде 1С — это попытаться выполнить чтение или установить явную управляемую блокировку в блоке Попытка... Исключение:


Попытка
    // Попытка наложения управляемой блокировки или тестовый быстрый запрос
    Блокировка = Новый БлокировкаДанных;
    ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ЦеныНоменклатуры");
    ЭлементБлокировки.УстановитьЗначение("Номенклатура", ВыбНоменклатура);
    Блокировка.Заблокировать();
Исключение
    // Сюда мы попадем, только если время ожидания блокировки УЖЕ истекло
    ЗаписатьВЛог("Регистр заблокирован, операция отложена.");
КонецПопытки;

Однако этот подход имеет критический недостаток: метод Заблокировать() или выполнение запроса не возвращают статус мгновенно. Система зависнет на установленный таймаут (по умолчанию 20 секунд) в ожидании освобождения ресурса и только потом вызовет исключение. Это не решает проблему зависания интерфейса у пользователей.

Давайте выясним истинную причину проблемы на конкретном примере запроса к регистру цен.

Анализ уязвимости в запросе к «СрезПоследних»

Рассмотрим код запроса, из-за которого чаще всего возникает ошибка таймаута блокировки:


Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1
|    ЦеныНоменклатурыСрезПоследних.ТипЦен КАК ТипЦен
|ИЗ
|    РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
|        &КонПериода,
|        Цена = &Цена И Номенклатура = &Номенклатура) КАК ЦеныНоменклатурыСрезПоследних";

В этом запросе кроется серьезная архитектурная уязвимость, приводящая к избыточным блокировкам в СУБД:

  1. Фильтрация по ресурсу в параметрах виртуальной таблицы. Поле Цена является ресурсом (или реквизитом) регистра сведений ЦеныНоменклатуры, а не его измерением. Индексы периодического регистра сведений строятся СУБД строго по схеме Период + Измерения. Ресурсы в индексах не участвуют.
  2. Сканирование таблицы (Table Scan / Index Scan). Передавая условие Цена = &Цена внутрь параметров виртуальной таблицы СрезПоследних, мы заставляем СУБД выполнять полное сканирование физической таблицы регистра. Система не может использовать индекс для поиска по цене и вынуждена перебирать огромный объем записей.
  3. Эскалация блокировок СУБД. Во время сканирования SQL-сервер накладывает разделяемые блокировки (Shared Locks — S) на считываемые строки. Когда количество мелких строчных блокировок превышает порог (обычно около 5000 блокировок на одну таблицу), СУБД выполняет эскалацию блокировок — заменяет тысячи строчных блокировок на одну блокировку всей таблицы (TABLOCK).
  4. Конфликт «Писатель-Читатель». Как только СУБД накладывает монопольную или разделяемую блокировку на всю таблицу регистра цен, любые документы, пытающиеся записать в этот момент новые цены (требующие монопольной блокировки X), выстраиваются в очередь. И наоборот: если документ проводит транзакцию записи, ваш «легкий» запрос на чтение блокируется намертво и завершается по таймауту.

Способы решения проблемы и оптимизации

Для устранения этой проблемы рассмотрим три эффективных направления оптимизации.

Решение 1. Оптимизация текста запроса и отборов

Самый простой и быстрый шаг — убрать неиндексируемые поля из параметров виртуальной таблицы. В параметрах СрезПоследних должны оставаться только индексируемые поля (измерения). Фильтрацию по ресурсам необходимо выносить во внешнее условие ГДЕ.

Перепишем наш запрос правильно:


Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1
|    ЦеныНоменклатурыСрезПоследних.ТипЦен КАК ТипЦен
|ИЗ
|    РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&КонПериода, Номенклатура = &Номенклатура) КАК ЦеныНоменклатурыСрезПоследних
|ГДЕ
|    ЦеныНоменклатурыСрезПоследних.Цена = &Цена";

При таком подходе СУБД сначала мгновенно найдет срез последних цен по конкретной номенклатуре, используя эффективный поиск по индексу (Index Seek), а затем быстро отфильтрует полученный результат по значению цены на уровне временной выборки, не накладывая избыточных блокировок на всю таблицу.

Решение 2. Использование метода «ПолучитьПоследнее()»

Если перед нами стоит задача получить данные только по одной конкретной позиции номенклатуры, то вместо написания тяжелого SQL-запроса гораздо эффективнее применить оптимизированный метод менеджера регистра сведений ПолучитьПоследнее().

Выполним это по шагам:


// Шаг 1. Создаем структуру отбора по ключевым измерениям
Отбор = Новый Структура;
Отбор.Вставить("Номенклатура", ВыбНоменклатура);

// Шаг 2. Получаем структуру последних значений напрямую из базы данных
ПоследниеДанные = РегистрыСведений.ЦеныНоменклатуры.ПолучитьПоследнее(КонПериода, Отбор);

// Шаг 3. Выполняем проверку ресурса в коде
Если ПоследниеДанные.Цена = ВыбЦена Тогда
    ТипЦен = ПоследниеДанные.ТипЦен;
КонецЕсли;

Этот метод платформы спроектирован для максимально быстрого точечного извлечения записей. Он работает значительно быстрее и безопаснее с точки зрения блокировок СУБД, чем произвольный ручной запрос.

Решение 3. Перевод конфигурации на управляемые блокировки и включение RCSI

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

Проанализируем шаги системной настройки для полного устранения конфликтов «писатель-читатель»:

  1. Переведите конфигурацию 1С в режим управляемых блокировок. В управляемом режиме простые запросы на чтение вообще не накладывают транзакционных блокировок на уровне СУБД (за исключением случаев, когда разработчик явно указал ключевое слово ДЛЯ ИЗМЕНЕНИЯ).
  2. Включите режим RCSI (Read Committed Snapshot Isolation) на MS SQL Server. Это стандартная и обязательная рекомендация для всех баз данных 1С, работающих под управлением Microsoft SQL Server в управляемом режиме.

Для включения версионирования строк (RCSI) администратору базы данных необходимо выполнить следующий SQL-скрипт в среде SQL Server Management Studio (SSMS):


-- Переводим БД в режим изоляции моментального снимка транзакции
ALTER DATABASE [Имя_Вашей_Базы_1С] SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;

После включения этого режима читающие транзакции начинают бесконфликтно читать согласованные копии данных из версионного хранилища (базы TempDB), совершенно не блокируя транзакции записи, а пишущие транзакции перестают блокировать чтение. Это полностью исключает возникновение ошибки с кодом native=1222 при обычных запросах к регистру.

Мониторинг блокировок в реальном времени

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

В MS SQL Server вы можете запустить быстрый системный запрос для поиска виновника блокировки:


-- Посмотреть активные ожидания блокировок и заблокированные сессии (SPID)
SELECT 
    session_id AS Spid,
    blocking_session_id AS BlockingSpid,
    wait_type,
    wait_time / 1000 AS WaitTimeSec,
    resource_description
FROM sys.dm_os_waiting_tasks
WHERE blocking_session_id IS NOT NULL;

Этот запрос наглядно покажет идентификатор сессии-виновника (BlockingSpid), из-за которого ваш запрос к ценам ожидает освобождения ресурса. Для детального сопоставления SQL-сессии с конкретным пользователем 1С рекомендуется использовать консоль «Кластер серверов 1С:Предприятия».

← На главную