В процессе активной многопользовательской работы в конфигурациях на платформе 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
| ЦеныНоменклатурыСрезПоследних.ТипЦен КАК ТипЦен
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
| &КонПериода,
| Цена = &Цена И Номенклатура = &Номенклатура) КАК ЦеныНоменклатурыСрезПоследних";
В этом запросе кроется серьезная архитектурная уязвимость, приводящая к избыточным блокировкам в СУБД:
Цена является ресурсом (или реквизитом) регистра сведений ЦеныНоменклатуры, а не его измерением. Индексы периодического регистра сведений строятся СУБД строго по схеме Период + Измерения. Ресурсы в индексах не участвуют.Цена = &Цена внутрь параметров виртуальной таблицы СрезПоследних, мы заставляем СУБД выполнять полное сканирование физической таблицы регистра. Система не может использовать индекс для поиска по цене и вынуждена перебирать огромный объем записей.S) на считываемые строки. Когда количество мелких строчных блокировок превышает порог (обычно около 5000 блокировок на одну таблицу), СУБД выполняет эскалацию блокировок — заменяет тысячи строчных блокировок на одну блокировку всей таблицы (TABLOCK).X), выстраиваются в очередь. И наоборот: если документ проводит транзакцию записи, ваш «легкий» запрос на чтение блокируется намертво и завершается по таймауту.Для устранения этой проблемы рассмотрим три эффективных направления оптимизации.
Самый простой и быстрый шаг — убрать неиндексируемые поля из параметров виртуальной таблицы. В параметрах СрезПоследних должны оставаться только индексируемые поля (измерения). Фильтрацию по ресурсам необходимо выносить во внешнее условие ГДЕ.
Перепишем наш запрос правильно:
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1
| ЦеныНоменклатурыСрезПоследних.ТипЦен КАК ТипЦен
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&КонПериода, Номенклатура = &Номенклатура) КАК ЦеныНоменклатурыСрезПоследних
|ГДЕ
| ЦеныНоменклатурыСрезПоследних.Цена = &Цена";
При таком подходе СУБД сначала мгновенно найдет срез последних цен по конкретной номенклатуре, используя эффективный поиск по индексу (Index Seek), а затем быстро отфильтрует полученный результат по значению цены на уровне временной выборки, не накладывая избыточных блокировок на всю таблицу.
Если перед нами стоит задача получить данные только по одной конкретной позиции номенклатуры, то вместо написания тяжелого SQL-запроса гораздо эффективнее применить оптимизированный метод менеджера регистра сведений ПолучитьПоследнее().
Выполним это по шагам:
// Шаг 1. Создаем структуру отбора по ключевым измерениям
Отбор = Новый Структура;
Отбор.Вставить("Номенклатура", ВыбНоменклатура);
// Шаг 2. Получаем структуру последних значений напрямую из базы данных
ПоследниеДанные = РегистрыСведений.ЦеныНоменклатуры.ПолучитьПоследнее(КонПериода, Отбор);
// Шаг 3. Выполняем проверку ресурса в коде
Если ПоследниеДанные.Цена = ВыбЦена Тогда
ТипЦен = ПоследниеДанные.ТипЦен;
КонецЕсли;
Этот метод платформы спроектирован для максимально быстрого точечного извлечения записей. Он работает значительно быстрее и безопаснее с точки зрения блокировок СУБД, чем произвольный ручной запрос.
Если ваша база данных все еще работает в автоматическом режиме блокировок, то абсолютно любое чтение данных внутри транзакции будет накладывать разделяемые блокировки (S) на уровне СУБД, приводя к конфликтам с пишущими транзакциями.
Проанализируем шаги системной настройки для полного устранения конфликтов «писатель-читатель»:
ДЛЯ ИЗМЕНЕНИЯ).Для включения версионирования строк (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С:Предприятия».