gifts2017

Планы обмена. Управляемый режим блокировок

Опубликовал Дмитрий Жичкин (zhichkin) в раздел Администрирование - Распределенная БД (УРИБ, УРБД)

Статья о том, как устроен объект конфигурации 1С:Предприятие 8 "План обмена", в том числе на уровне СУБД SQL Server. Анализируются особенности его использования при управляемом режиме блокировок.

Механизм обмена данными платформы 1С:Предприятие 8 основан на объектах конфигурации, которые называются "план обмена". Этот механизм является основным и призван решать следующие задачи: регистрация изменений объектов и обеспечение обмена сообщениями между узлами распределённой информационной системы. Данная статья предполагает, что читатель уже знаком с основными понятиями и имеет опыт использования планов обмена.

Работа с РИБ в рамках данной статьи рассмотрена не будет, так как РИБ полностью основан на описанных в статье механизмах.

Структуры данных.

План обмена на уровне СУБД это таблица, которая имеет название вида "_Node[N]", где N - это внутренний числовой код типа данных 1С:Предприятие 8. В данном случае этот код будет соответствовать плану обмена.

Создадим тестовый план обмена. В конфигураторе 1С (изображение слева) и в SQL Server Management Studio (изображение справа) это будет выглять следующим образом:

Как мы видим по структуре таблицы справа, план обмена очень похож на справочник. Это тоже ссылочный тип данных. Однако, от справочников его отличает то, что он не может иметь групп и быть иерархическим.

Структура таблицы "_Node69" плана обмена "Тестовый"

  Наименование поля      Имя реквизита   Описание поля
 _IDRRef  Ссылка  Ссылка на узел плана обмена
 _Version  Версия записи таблицы СУБД
 _Marked  ПометкаУдаления  Пометка удаления
 _Code  Код  Код узла
 _Description  Наименование  Наименование узла
 _SentNo  НомерОтправленного    Номер отправленного сообщения
 _ReceivedNo  НомерПринятого  Номер принятого сообщения
 _PredefinedID  Идентификатор предопределённого элемента  

Следует отметить, что при создании плана обмена, платформа автоматически создаёт предопределённый узел, соответствующий текущей информационной базе, в которой он создаётся. Реквизиты "Код" и "Наименование" по умолчанию имеют пустые значения, их необходимо заполнить самостоятельно. Для того, чтобы убедиться в этом, выполним следующий код в SQL Server Management Studio:

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

Структура таблицы "_ReferenceChngR71" справочника "Номенклатура".

Наименование поля Имя Реквизита Описание поля
 _NodeTRef  Узел  Код типа плана обмена, например, у плана обмена "Тестовый" этот код равен значению "69".
 _NodeRRef  Узел  Ссылка на узел плана обмена
 _MessageNo  НомерСообщения   Номер сообщения, в котором данное изменение было выгружено для соответствующего узла плана обмена
 _IDRRef  Ссылка  Ссылка на элемент справочника "Номенклатура", который изменился

Таким образом, таблица регистрации изменений состоит из трёх обязательных полей и одного и более полей, которые необходимы для поиска изменившихся данных в основной таблице объекта. Для ссылочных типов данных таких, как справочники или документы, достаточно одного поля - "Ссылка". Однако для табличных типов данных, как, например, регистры сведений или регистры накопления, этих полей может быть несколько. Это зависит от установленного для измерений таких объектов свойства "Основной отбор". Более подробно об этом можно прочитать в книге "Профессиональная разработка в системе 1С:Предприятие 8. Издание 2." на странице 484.

По умолчанию планы обмена используют автоматическую регистрацию изменений объектов, которые включены в их состав. Однако это поведение можно изменить. В таком случае придётся использовать программную (ручную) регистрацию. Отключить авторегистрацию можно так, как это показано на картинке ниже.

Индексы таблиц регистрации изменений.

Для полноты картины приведу здесь информацию об индексах, которые создаёт платформа для таблиц регистрации изменений. Комментировать ничего не буду, так как, по моему и так всё понятно. Их всего два:

1. Ключ + Узел (кластерный).

2. Узел + Номер сообщения + Ключ.

Момент выполнения регистрации изменений.

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

1. Начало транзакции записи документа.

2. ПередЗаписью.

3. ПередЗаписью (подписка на событие).

4. ПриЗаписи.

5. ПриЗаписи (подписка на событие).

6. ОбработкаПроведения.

7. ОбработкаПроведения (подписка на событие).

8. Регистрация изменения объекта во всех планах обмена, в состав которых он входит, а также для всех узлов этих планов обмена.

9. Фиксация транзакции записи документа.

Почему важно понимать в какой момент происходит регистрация изменений? И почему я привёл в пример именно документ? Дело в том, что регистрация изменений выполняется в одной транзакции с записью и проведением документа. Это обеспечивает согласованность этих двух операций: проведения и регистрации. Следовательно, любая логика регистрации изменений влияет на продолжительность этой транзакции, а также на момент выполнения запроса на блокировку записей таблицы регистрации изменений. В высоко нагруженных системах такой запрос часто приводит к ожиданию на блокировках, которые ранее уже наложила, например, процедура выгрузки изменений. Далее в статье я расскажу об этом более подробно.

Регистрация изменений.

Для регистрации изменения выполним следующий код 1С:

Если мы воспользуемся SQL Server Profiler и посмотрим, что происходит при вызове процедуры "ЗарегистрироватьИзменения", то мы увидим следующий код (для удобства восприятия код немного "причёсан"):

Как мы видим, на 2-ом шаге транзакции 1С сразу же пытается наложить эксклюзивную блокировку на запись регистрации изменения элемента справочника "Номенклатура" по соответствующему узлу плана обмена. Кроме этого, предпринимается попытка установить своеобразный флаг, сигнализирующий о том, что это изменение необходимо выгрузить в новом/следующем сообщении обмена. Этим флагом является реквизит "НомерСообщения" (поле "_MessageNo"), значение которого устанавливается равным NULL.

Обратите внимание, что если мы изменим один и тот же объект несколько раз, то запись изменений для этого объекта будет только одна. Это ключевая особенность планов обмена! Фактически это делает такую запись дефицитным ресурсом, за захват которого конкурируют разные транзакции. По сути, если вы знакомы с проблемой блокировок при обновлении таблицы итогов регистров накопления, то здесь вы увидите схожую ситуацию. Однако, если в случае с регистрами накопления мы можем использовать режим разделения итогов, чтобы распраллелить хотя бы запись в таблицу итогов, то здесь этой возможности нет. Кроме этого, если за захват записей таблицы итогов конкурируют между собой только документы, то в случае с записями таблиц регистрации изменений конкурирующих между собой объектов гораздо больше. Практически это все объекты, включённые в состав плана обмена.

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

Кроме этого очень важно то, что изменение регистрируется для всех узлов плана обмена. То есть если бы в нашем плане обмена было 10 узлов, то изменение одного объекта потребовало бы создания 10 записей, 100 узлов - 100 записей и так далее. И это только для одного изменения. Этот факт тоже важно учитывать.

Просмотр изменений.

Теперь попробуем изменить что-нибудь в нашем справочнике "Номенклатура" и посмотрим как это отразится в таблице регистрации изменений. Для этого можно воспользоваться следующими запросами 1С или T-SQL:

Запрос = Новый Запрос();
Запрос.Текст = "ВЫБРАТЬ * ИЗ Справочник.Номенклатура.Изменения";
SELECT * FROM [_ReferenceChngR71]

Однако, мы ничего не увидим ... Дело в том, что регистрация изменений выполняется только в контексте узлов плана обмена. Это ещё одна ключевая особенность этого объекта конфигурации: все изменения регистрируются только в контексте какого-нибудь узла обмена. Поэтому добавляем новый узел в созданный ранее план обмена "Тестовый", и пытаемся изменить что-нибудь в справочнике "Номенклатура" снова. После этого проверяем таблицы изменений ещё раз и видим, что там появились записи.

Удаление регистрации изменений.

Теперь, чтобы удалить регистрацию изменения, выполним такой код 1С:

На этот раз 1С ограничивается одной командой SQL, пытаясь наложить всю ту же самую эксклюзивную блокировку по тому же самому ключу, что в общем-то ожидаемо и вполне нормально:

Запись изменений в сообщение обмена.

Для того, чтобы выгрузить изменения в другой узел распределённой информационной системы, необходимо эти изменения сначала прочитать, а затем записать в сообщение обмена. Механизм обмена сообщениями тесно интегрирован с планами обмена. В данном случае имеется в виду то, что запись сообщения обмена в файл сопровождается изменением реквизита "НомерСообщения" в таблице регистрации изменений объекта и ревизита "НомерОтправленного" в таблице плана обмена для соответствующего узла, для которого выполняется выгрузка данных. Чтобы реализовать всё это необходимо использовать объект "ЗаписьСообщенияОбмена". Именно этот объект позволяет нам управлять номером отправляемого сообщения: получать новый порядковый номер, записывать его в сообщение обмена, в таблицу регистрации изменений и реквизит узла плана обмена.

Чтобы исследовать код, который необходимо выполнить для записи сообщения обмена, я настроил технологический журнал 1С на регистрацию событий TLOCK, EXCP и TTIMEOUT. Эти события позволяют анализировать установление управляемых блокировок и ошибок, связанных с неудачными попытками установить их. Кроме этого я настроил SQL Server Profiler, чтобы посмотреть какие запросы платформа 1С генерирует на уровне СУБД.

Версия платформы 1С:Предприятие 8.3.7.2027. Управляемый режим блокировок.

// 0. Подготавливаем необходимые объекты.
ФайлСообщенияОбмена = "C:\export\message.xml";
ЗаписьXML = Новый ЗаписьXML();
ЗаписьXML.ОткрытьФайл(ФайлСообщенияОбмена);

УзелОбмена = ПланыОбмена.Тестовый.НайтиПоНаименованию("Тестовый узел", Истина);

// 1. Создаём объект "ЗаписьСообщенияОбмена".
ЗаписьСообщения = ПланыОбмена.СоздатьЗаписьСообщения();

// 2. Начинаем запись сообщения для выбранного узла в файл XML.
ЗаписьСообщения.НачатьЗапись(ЗаписьXML, УзелОбмена);

В книге "Профессиональная разработка в системе "1С:Предприятие 8" (издание 2) от 2012 года на странице 491 написано следующее:

При выполнении этого метода сообщению присваивается номер, определяемый как номер предыдущего отправленного сообщения, увеличенный на 1 (информация берётся из узла-получателя). Производится запись в XML-документ заголовка сообщения, а также записывается начало элемента XML, соответствующего телу сообщения. Устанавливается блокировка на запись базы данных, соответствующая узлу плана обмена.

Если посмотреть ТЖ 1С, то там мы не увидим установки блокировки. Значит управляемая блокировка не устанавливается. На уровне СУБД мы увидим обычный запрос, который характерен для вызова метода ссылки "ПолучитьОбъект":

SELECT
	T1._IDRRef,
	T1._Version,
	T1._Marked,
	T1._Code,
	T1._Description,
	T1._SentNo,
	T1._ReceivedNo,
	T1._PredefinedID
FROM
	dbo._Node69 T1 -- План обмена "Тестовый"
WHERE
	T1._IDRRef = 0x9BD9408D5C93CC8E11E6A9E8C5C37A96 -- Узел обмена "Тестовый узел"

Постольку поскольку наша конфигурация находится в режиме управляемых блокировок, то такой запрос не является блокирующим. Блокировок нет. В книге ошибка? Давайте попробуем зарегистрировать какое-нибудь изменение по нашему узлу. Воспользуемся кодом из раздела "Регистрация изменений". Но сначала убедимся, что изменение уже есть, выполнив код SQL:

Получается в данный момент мы пытаемся выгрузить сообщение, которое ранее уже выгружалось под номером 13. Повторная выгрузка. В данный момент первая сессия 1С остановлена в точке останова после выполнения метода "НачатьЗапись". Теперь откроем вторую сессию 1С и попробуем зарегистрировать изменение этого объекта ещё раз в тот момент как оно отправляется:

ПланыОбмена.ЗарегистрироватьИзменения(УзелОбмена, СсылкаДляОбмена);

Теперь проверим, что изменилось:

Ошибок блокировки не было. Поле "_MessageNo" изменилось, сигнализируя нам, что его необходимо выгрузить. Так какая же блокировка имелась ввиду в книге? Неужели всё-таки ошибка? Ошибки никакой нет. Чтобы убедиться в этом выполним в сессии № 2 следующий код 1С:

Узел = ПланыОбмена.Тестовый.НайтиПоНаименованию("Тестовый узел", Истина);
Объект = Узел.ПолучитьОбъект();
Объект.Заблокировать();

В результате мы получим следующее сообщение об ошибке (эту же ошибку мы увидим в ТЖ, событие EXCP):

{ВнешняяОбработка.ИсследованиеПлановОбмена.МодульОбъекта(103)}: Ошибка при вызове метода контекста (Заблокировать): Не удалось заблокировать запись. Действие (изменение, удаление или блокировка записи) не выполнено.
Ошибка блокировки объекта. Объект уже заблокирован:
компьютер: Zhichkin, сеанс: 45, начат: 24.11.2016 в 1:00:46, приложение: Толстый клиент

Получается, что автор книги имел в виду объектную блокировку узла, а не блокировку записи базы данных. Следовательно, для того, чтобы убедиться в том, что выполняется выгрузка изменений по узлу, необходимо выполнить выше приведённый код и, если возникнет ошибка блокировки, то значит так оно и есть. Посмотрим что будет происходить дальше:

// 3. Выбираем изменения для выгрузки
Выборка = ПланыОбмена.ВыбратьИзменения(УзелОбмена, ЗаписьСообщения.НомерСообщения);
BEGIN TRANSACTION

UPDATE
	[_ReferenceChngR71] -- Таблица регистрации изменений
SET
	[_MessageNo] = 26 -- Очередной номер отправленного сообщения
WHERE
	[_NodeTRef] = 0x00000045 -- План обмена "Тестовый"
	AND
	[_NodeRRef] = 0x9BD9408D5C93CC8E11E6A9E8C5C37A96 -- Узел "Тестовый узел"
	AND
	[_MessageNo] IS NULL -- Устанавливаем номер для ещё не отправленных сообщений

-- Выбираем все изменения, в том числе отправленные в других сообщениях
SELECT
	[_IDRRef]
FROM
	[_ReferenceChngR71]
WHERE
	[_NodeTRef] = 0x00000045
	AND
	[_NodeRRef] = 0x9BD9408D5C93CC8E11E6A9E8C5C37A96

COMMIT TRANSACTION

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

При этом если мы зарегистрируем по этому узлу и типу объекта новое изменение, то оно успешно запишется. Более того, если это произойдёт до команды SELECT, то в выборку изменений попадут и эти новые изменения. Что интересно номер отправленного у них будет равен NULL, хотя это совершенно не так.

Кроме этого если записей будет более 5000, то может произойти эскалация блокировок SQL Server. В таком случае может оказаться заблокированной вся таблица. Чтобы этого не происходило, администраторы баз данных часто отключают на уровне SQL возможность возникновения такой эскалации, однако это не запрещает эскалации до уровня страниц (имеют размер 8 Kb), что может приводить к избыточной блокировке записей не попадающих в условия отбора команды UPDATE, но находящихся на одной странице вместе с нужными записями.

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

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

2. Избыточные блокировки при больших объёмах изменений из-за эскалации блокировок SQL Server. Усугубляет последствия первого пункта.

3. Рассогласованность нумерации сообщений для новых регистраций объектов, которые не попали в первоначальный UPDATE. Следствием этого может быть повторная выгрузка одних и тех же данных.

Все выбранные изменения обрабатываются в оперативной памяти. Это происходит в следующем цикле:

// 4. Обработка выбранных изменений
Пока Выборка.Следующий() Цикл
	Данные = Выборка.Получить();
	ЗаписатьXML(ЗаписьXML, Данные);
КонецЦикла;

На этом этапе на уровне SQLServer ничего интересного не происходит: выполнение метода Выборка.Получить() получает данные объекта по его ссылке так, как если бы мы вызвали метод Ссылка.ПолучитьОбъект() или Набор.Прочитать(). Единственным недостатком можно назвать то, что это выполняется в цикле для каждого объекта, что трудно назвать оптимальным решением. Каждый объект записывается в сообщение обмена в формате XML. Это самый длительный этап выгрузки данных. К сожалению его практически невозможно распараллелить, так как запись ведётся в один файл.

В случае успешной записи сообщения обмена, завершаем сеанс выгрузки данных:

// 5. Завершаем сеанс выгрузки данных
ЗаписьСообщения.ЗакончитьЗапись();
UPDATE
	[_Node69] -- План обмена "Тестовый"
SET
	[_Marked]       = 0x00,
	[_Code]         = N'1',
	[_Description]  = N'Тестовый узел',
	[_SentNo]       = 26, -- Номер следующего сообщения обмена
	[_ReceivedNo]   = 0,
	[_PredefinedID] = 0x00000000000000000000000000000000
WHERE
	[_IDRRef] = 0x9BD9408D5C93CC8E11E6A9E8C5C37A96 -- Узел "Тестовый узел"
	AND
	[_Version] = 0x00000000000077E8

Завершение сеанса выгрузки заключается в том, что для соответствующего узла плана обмена вычисляется номер следующего сообщения обмена и вызывается метод объекта "Записать". В результате этого на узел плана обмена устанавливается управляемая блокировка - в техническом журнале 1С фиксируется событие TLOCK. Это очень короткая транзакция.

Чтение изменений из сообщения обмена.

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

ЧтениеXML = Новый ЧтениеXML();
ЧтениеXML.ОткрытьФайл(ПолноеИмяФайла);
ЧтениеСообщения = ПланыОбмена.СоздатьЧтениеСообщения();
ЧтениеСообщения.НачатьЧтение(ЧтениеXML, ДопустимыйНомерСообщения.Больший);
Пока ВозможностьЧтенияXML(ЧтениеXML) Цикл
    Данные = ПрочитатьXML(ЧтениеXML);
    Данные.ОбменДанными.Отправитель = ЧтениеСообщения.Отправитель;
    Данные.ОбменДанными.Загрузка = Истина;
    Данные.Записать();
КонецЦикла;
//ПланыОбмена.УдалитьРегистрациюИзменений(ЧтениеСообщения.Отправитель, ЧтениеСообщения.НомерПринятого); 
ЧтениеСообщения.ЗакончитьЧтение();

Заключение.

Плюсы планов обмена:

    1. Простота программирования и настройки.

Минусы планов обмена:

    1. Избыточные блокировки данных.

    2. Конфликты блокировок транзакций.

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

    4. Гранулярность обработки данных - узел обмена.

    5. Дублирование выгрузки данных.

    6. Плохие возможности для распараллеливания процессов.

    7. Тесная интеграция механизма регистрации изменений и инфраструктуры сообщений (их нумерация) диктует свои правила и ограничивает возможности реализации каких-то своих оптимизационных решений. См. также пункт 3.

См. также

Ошибка! Указанная тема не найдена.