"Гибкие" блокировки в 1С 7.7 и MS SQL Server   

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

 

 

 

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

Например, в хранимой процедуре _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");
КонецЕсли;
КонецЕсли;
глСКЛ.Закрыть();

КонецПроцедуры

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

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

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

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

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

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

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

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

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

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

При движении по регистру вызывается ряд хранимых процедур но показательные из них две это _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С 7.7.

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

При использовании материалов данной статьи обязательна ссылка на информационный ресурс.

По интересующим Вас вопросам просьба обращаться на softpoint@softpoint.ru

 

Статьи по этой теме:

Что такое ошибка при транзакции, выполняемой другим пользователем? 

"Внутренние" блокировки в 1С

Технология "Гибкие блокировки"

Экономическая эффективность внедрения технологии "Гибкие блокировки"

Как внедрить

 

 


 

Перепечатка, воспроизведение в любой форме, распространение, в том числе в переводе, любых материалов с сайта www.softpoint.ru возможны только с письменного разрешения компании "СофтПоинт". Это правило действует для всех без исключения случаев, кроме тех, когда в материале прямо указано разрешение на копирование (основание: Закон Российской Федерации "Об авторском праве и смежных правах").