«Гибкие» блокировки в 1С

Как то помню на форуме прочитал сообщение типа «а вот знакомый админ удалил Tablockx и поставил rowlock в хранимых процедурах и все закрутилось, завертелось»… Эта мысль по-моему достаточно показательна для многих из 1С программистов и особенно для новичков.

Материал опубликован на сайте www.softpoint.ru/

Как то помню на форуме прочитал сообщение типа «а вот знакомый админ удалил Tablockx и поставил rowlock в хранимых процедурах и все закрутилось, завертелось»… Эта мысль по-моему достаточно показательна для многих из 1С программистов и особенно для новичков.

Автор статьи - Сердюк В.И.

Для того чтобы не наломать дров в вашей ИТ системе необходимо : во первых понимать для чего существуют блокировки в М S SQL server , во вторых понимать как устроен блокировочный механизм в 1С.

По первой части есть масса литературы и поэтому не хотелось бы ее пересказывать…Отмечу лишь принципиальные моменты…

В MS SQL есть понятие блокировок и подсказок блокировок. Основное предназначение избежать проблем некорректного(грязное чтение, чтение фантомов и т.п.) чтения информации.

Для 1С значимы следующие виды блокировок

Holdlock – Захватывает разделяемую блокировку до завершения транзакции.

Nolock - Приминима только к оператору select . Читает все…

Tablock - Используется блокировка на уровне таблиц.

Tablockx - Используется монопольная блокировка таблицы.

Отдельно выделю подсказки блокировки которые «горячие» головы рекомендуют применять

Rowlock - блокировка на уровне строк

Updlock - блокировка на изменение

Readpast - Приминима только для Select . Пропускаются строки блокированные( rowlock ) другими транзакциями.

Для того что бы понять как действуют эти блокировки необходимо почитать соответствующую литературу а еще лучше самим проверить на практике эти блокировки в различных ситуациях…

На специфике реализации блокировок в 1С остановлюсь подробнее.

Механизм блокировок в 1С максимально простой - блокируется все и по максимуму.

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

Начнем с того что в 1С существует специальная таблица _1 sjourn где хранятся внутренние идентификаторы всех документов. При записи, проведении и т.п. операциях с документами 1С накладывает блокировку на таблицу _1 sjourn и соответственно в системе в один момент времени может проводиться не более одного документа. То есть _1 sjourn выступает в роли своеобразного семафора. До тех пор пока не завершиться транзакция и соответственно не будет снята блокировка с таблицы все остальные клиенты будут ждать разблокировки и самое интересное как это они будут делать. В момент ожидания 1С загружает ресурсы сервера, т.к. непрерывно сканирует таблицу на вопрос разблокировки и поэтому загружает процессор по максимуму.

Удалить механизм блокировок можно путем изменения хранимых процедур, через которые 1С проверяют таблицы на блокировки. А точнее, удалить хинты на блокировку таблиц.

Например, в хранимой процедуре _1 sp __1 SJOURN _ TLockX в конструкции
select @i=1 from _1SJOURN(TABLOCKX HOLDLOCK) where 0=1
необходимо удалить TABLOCKX HOLDLOCK.

Также необходимо удалить соответствующие хинты(подсказки блокировки) в остальных таблицах относящихся к документам. Если этого не сделать то будут возникать deadlock -и – взаимоблокировки транзакций на уровне SQL server .

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

Создадим конфигурацию : справочник - Товары, документ – ПриходнаяНакладная, документ – РасходнаяНакладная, регистр – ОстаткиТовара Приходная накладная нужна только для того что бы сделать одно единственное движение. Больше логической нагрузки она нести не будет.

В модуле расходной накладной реализуем следующую логику :

Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса =
"//{{ЗАПРОС(Сформировать)
|Товар = Регистр.Остатки.Товар;
|Количество = Регистр.Остатки.Количество;
|Функция КоличествоКонОст = КонОст(Количество);
|Группировка Товар;
|"//}}ЗАПРОС
;

Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Возврат;
КонецЕсли;

Сообщить(НомерДок);
Если Число(НомерДок)=1 Тогда
Для Сч6=1 По 10000 Цикл
Сообщить("Ждем второго. Вообще то здесь может идти
| какой-то анализ или обработка данных.
|Чем больше интервал между получением остатков и их
| анализом с последующей записью движений -
|тем больше вероятность возникновения отрицательных остатков");
КонецЦИкла;
КонецЕсли;



Запрос.Группировка(1);
Если (Запрос.КоличествоКонОст<0) Тогда
Сообщить("Ошибка!!!");
Возврат;
КонецЕсли;

Если Запрос.КоличествоКонОст>=Количество Тогда
Регистр.Остатки.Товар=Товар;
Регистр.Остатки.Количество=Количество;
Регистр.Остатки.ДвижениеРасходВыполнить();
Иначе

Сообщить("Не хватает!");
Возврат;
КонецЕсли;

 

Товар это у меня реквизит шапки и поэтому я использую одномерную (например использую Запрос.Группировка(1) зная что движение по одному товару) модель. Для многомерной модели ничего принципиально не меняется…

Логика вкратце сводится к тому что необходимо перед тем как списывать товар провести анализ , а хватает ли его на складе? Казалось бы при такой модели не должно возникать отрицательных остатков – ведь стоит проверка. Это возникающее на первый взгляд впечатление ошибочно. Для доказательства этого я вставил в середине модуля обработку которая эмулирует задержку между получением остатков и их последующим анализом и записью движений. Получается следующая ситуация когда два пользователя одновременно пытаются списать товара в сумме больше чем есть на складе(в хронологическом порядке)

  • Первый пользователь получает остатки
  • Второй пользователь получает остатки
  • У первого пользователя идет какая либо обработка.(в моем случае выводится сообщение)
  • Второй пользователь проводит проверку остатков и списывает товар. Конец проведения.
  • Первый пользователь проводит анализ остатков и тут возникает самое интересное – информация по остаткам уже устарела! Но первый пользователь об этом не подозревает , проходит проверка остатка и списание. КонецПроведения.

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

Как избежать этой неприятной ситуации? – Реализовать свой, гибкий механизм блокировок.

Вот пример:

Перем глТои Экспорт;

Процедура ПроверкаБлокировкиРесурса(Ресурс) Экспорт;
глТои.Закрыть();
глТои.Соединиться(0);
//глТои.ВыполнитьЗапрос("set lock_timeout 20000");
Состояние("Проверка блокировки...");
Если Ресурс = "Регистр.ОстаткиТовара" Тогда
Если (глТои.ВыполнитьЗапрос("exec MyRA17_TLockX") = 1) тогда

Иначе
Сообщить("Ждет Занят MyRA17_TLockX");
КонецЕсли;
Если(глТои.ВыполнитьЗапрос("exec MyRG17_TLockX") = 1) тогда

Иначе
Сообщить("Ждет Занят MyRG17_TLockX");
КонецЕсли;
КонецЕсли;
глТои.Закрыть();

КонецПроцедуры
Загрузчик = CreateObject("Toy.Loader");
Хэндл = Загрузчик.LoadLibrary("C:Program Files1Cv77BINtoysql21.dll");
глТои = СоздатьОбъект("ToyQuery");

В данном случае я блокирую по объекту метаданных Регистр.ОстаткиТовара.

А хранимые процедуры MyRG17_TLockX и MyRA17_TLockX я полностью дублирую текстом старых RG17_TLockX и RA17_TLockX.

Вставляя процедуру ПроверкаБлокировкиРесурса( «Регистр.ОстаткиТовара» ) в начало модуля проведения мы добиваемся того что второй пользователь ожидает пока первый не завершит транзакцию и только после этого он может прочитать остатки товара. При этом не возникает неоправданной нагрузки процессора и документы проводятся последовательно но только в контексте движения по Регистр.Остатки.

Возникает вопрос а что делать если у нас происходит движение по бухгалтерским итогам? В этом случае движение затрагивает одну таблицу и она становиться узким местом системы.

Возникает соблазн использовать конструкцию rowlock - ведь судя по названию она и сможет устанавливать блокировку не монопольную на таблицу а построчную на изменяемые записи.

Тут нужно во первых понять как хранит 1С данные и какими запросами получает данные.

Я возьму для примера только регистры. Для бухгалтерских итогов концепция будет аналогична.

Нужно отметить , что данные по регистрам хранятся в агрегированном виде т.е остатки по измерения хранятся в нескольких записях соответствующим периодичности итогов. Возникает вопрос . А не изменится ли целостность и непротиворечивость агрегированных данных при отключении стандартного механизма блокировок.

Для ответа на этот вопрос нужно во первых понимать что SQL server сам расставляет блокировки объектов при изменении данных. Убедиться в этом можно изменив в транзакции запись таблицы(как правило на индексированные) и запустив sp _ lock . По результатам sp _ lock мы увидим что данная запись заблокирована как будто если бы мы в явном виде поставили подсказку rowlock .

Вкратце концепция изменения агрегированных данных(например регистра остатков) 1С следующая –

При движении по регистру вызывается ряд хранимых процедур но показательные из них две это _1sp_GetNextPeriod - обеспечивает последовательное обновление агрегированных данных и _1sp_RG17_Change – собственно изменят конкретную запись. Для этого используется следующая конструкция:

Update RG17
set SP19=Case When ABS(SP19+@p3)>9999999999 Then 9999999999 Else SP19+@p3 End
where PERIOD=@per AND SP18=@p1 AND SP24=@p2
if @@ROWCOUNT=0
insert into RG17 values(@per,@p1,@p2,@p3)

Как мы видим в конструкции Update нет хинта rowlock . Но по большому счету он и не нужен т.к. сервер сам расставляет блокировки по измененным записям. В этом опять таки не сложно убедиться если провести ряд тестов, вкратце их смысл искусственно создать ситуацию при которой одна сессия будет изменять запись и другая ее будет изменять но на основании прочитанных ранее устаревших данных. Вставка новых записей в агрегационную таблицу по документу происходит как правило не более одной(если конечно точка актуальности не установлена абы как) и соответственно ситуация когда один документ делает вставку( insert into RG 17 values (@ per ,@ p 1,@ p 2,@ p 3) ) а другой ее не видит практически невозможна(кроме того стоит ограничение на уровне уникальности индекса, для возможной повторной вставки).

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

Обращаясь к примеру с созданием процедуры ПроверкаБлокировкиРесурса продолжая аналогию можно прийти к выводу о достаточности реализации блокировки объекта данных. Ну например если мы хотим , что бы блокировалась не целиком таблица проводок а например только информации связанная со счетом 41 и 004 то нам достаточно заблокировать объект СчетПоКоду(«41») и СчетПоКоду(«004»). То есть если мы знаем, что расходная накладная использует данные по 41 и 004 счетам нам достаточно вставить в начале модуля проведения процедуру ПроверкаБлокировкиОбъекта(« СчетПоКоду(«41») ») и ПроверкаБлокировкиОбъекта(« СчетПоКоду(«004») ») .

Если же мы знаем, что для нас принципиальна информация по остаткам товара на складе, то соответственно ПроверкаБлокировкиОбъекта(« Склад ») . Таким образом все документы по одному складу будут проводиться по очереди а по разным складам параллельно. То же самое если мы хотим сделать блокировки по Товару. Тогда будет соответственно ПроверкаБлокировкиСпискаОбъектов(спТовара) – но в этом случае нужно понимать что чем больше информации по блокировкам тем больше нагрузка на сервер. Необходимо найти золотую середину.

Собственно а как все вышеперечисленное реализовать. Вариантов несколько. Но раз мы уже остановились на классическом блокировочном механизме MS SQL то можно воспользоваться процедурой sp_getapplock. Смысл ее в следующем – блокировка произвольного ресурса. Запустив sp _ lock мы можем убедиться что это всего лишь еще одна запись. То есть например блокировку по складу(в контексте остатков по складу) можно сделать следующим образом sp_getapplock 'Остатки**Склад**Основной', 'Exclusive', 'transaction' . Соответственно аналогичная процедура выполненная из Сессии №2 сессии будет ждать завершения транзакции открытой Сессией №1 если в ней была запущенна sp_getapplock такими же параметрами. Процедура ПроверкаБлокировкиОбъекта всего лишь преобразует объект в уникальный идентификатор и передает его в качестве параметра в sp_getapplock в открытой сессии 1С.

В конце статьи хочется отметить что для того что бы реализовать механизм «гибких» блокировок необходимы какие либо навыки в MSSQL server а также понимание внутренней структуры 1С. Например снимая хинты с процедуры _1sp__1SSYSTEM_TLock вы тем самим можете попасть в ситуацию когда два проведенных документа перемещающие ТА проведутся одновременно и один из них окажется позже точки актуальности или наоборот сняв хинты с _1sp__1SJOURN_TLock и не сняв их с _1sp_DT???_TLockX вы можете создать deadlock -и.

P.S. На данный момент у автора статьи имеется готовое решение которое анализирует конфигурацию. На основании этого снимает все необходимые блокировки и взамен предоставляет свои готовые процедуры, используя которые вы можете реализовать блокировочный механизм на свое усмотрение. При этом, вставив сгенерированный текст процедуры в процедуру ПриНачалеРаботыСистемы вы гарантированно не будете зависеть от структурных изменений в конфигурации.

Подробнее о "гибких" блокировках читайте на сайте www.softpoint.ru.

 

Начать дискуссию