Команда CONSUME, нюанс № 1
Для более глубокого понимания того, как работает команда CONSUME рассмотрим два практических примера: первый без использования опции WITH STRICT ORDER
(стандартное рекомендуемое применение), и второй - с использованием этой опции. Для демонстрации примеров будет использована Microsoft SQL Server Management Studio. Работа с базой данных 1С:Предприятие 8 будет выполняться в режиме управляемых блокировок, то есть у базы данных будет включена опция read committed snapshot
.
Поведение опции
WITH STRICT ORDER
, описанное в данной статье, относится только к Microsoft SQL Server. Использование этой опции в контексте PostgreSQL не имеет существенного значения. Если эта опция не используется, то поведение команды CONSUME для Microsoft SQL Server и PostgreSQL аналогично.
Прежде, чем начать, необходимо объяснить использование хинтов ROWLOCK
и READPAST
в коде SQL, который генерирует DaJet Script, для Microsoft SQL Server, а также FOR UPDATE
и SKIP LOCKED
для PostgreSQL.
Назначение хинтов ROWLOCK
и FOR UPDATE
в команде CONSUME - это намерение получить эксклюзивную блокировку записи таблицы-очереди, чтобы убедиться в том, что команда будет обрабатывать транзакционно зафиксированные данные. Это очень важно для избежания ситуации обработки “фантомных” данных, которые возможно не будут зафиксированы. То есть без получения эксклюзивной блокировки есть высокий риск отправить в целевую систему обмена данными такие записи таблицы-очереди, которые в конечном итоге не были созданы или имеют неактуальные данные.
В свою очередь хинты READPAST
и SKIP LOCKED
изменяют реакцию команды CONSUME на заблокированные другими транзакциями записи таблицы-очереди. Эти хинты “разрешают” команде CONSUME пропускать такие записи, не ожидая снятия блокировки, и продолжать обрабатывать следующие за ними по порядку доступные записи. Это позволяет повысить параллельность работы с таблицей-очередью в сценариях многопоточной обработки очереди несколькими командами CONSUME одновременно (разными скриптами). Здесь следует заметить, что побочным эффектом таких сценариев может являться нарушение последовательсности обработки данных. Не всегда это критично, но всё же.
Использование опции
WITH STRICT ORDER
убирает из кода SQL хинтыREADPAST
иSKIP LOCKED
. Таким образом это может привести к ожиданию на блокировках вплоть до получения ошибки превышения таймаута.
Объяснить зачем эта опция может быть нужна - цель данной статьи.
Для демонстрации примеров будет использован регистр сведений 1С:Предприятие 8 “ОчередьИсходящихСообщений”, имеющий следующую структуру метаданных:
Свойство | Назначение | Тип данных |
---|---|---|
НомерСообщения | Измерение | Число(15,0) |
ТипСообщения | Ресурс | Строка(1024) |
ТелоСообщения | Ресурс | Строка(0) |
Данный регистр (непериодический и независимый) будет иметь кластерный индекс по единственному полю “НомерСообщения”.
Пример № 1 без использования опции WITH STRICT ORDER
Шаг 1. Транзакция A: откроем новую панель запросов и выполним следующий код SQL. Этот код добавит три новых сообщения в таблицу-очередь и будет “спать” 30 секунд прежде, чем зафиксирует свою транзакцию. Таким образом мы эмулируем длинную траназкцию. Итогом выполнения скрипта будет “подвисание” панели запросов на 30 секунд.
BEGIN TRANSACTION;
SET NOCOUNT ON;
INSERT _InfoRg123 SELECT 1, 'type', 'body';
INSERT _InfoRg123 SELECT 2, 'type', 'body';
INSERT _InfoRg123 SELECT 3, 'type', 'body';
WAITFOR DELAY '00:00:30.000';
COMMIT TRANSACTION;
Шаг 2. Транзакция B: откроем новое окно панели запросов и выполним следующий код SQL. Этот код также добавит три новых сообщения в таблицу-очередь, но зафиксирует свою транзакцию сразу без ожидания. Итогом выполнения скрипта будет моментальное его завершение.
BEGIN TRANSACTION;
SET NOCOUNT ON;
INSERT _InfoRg123 SELECT 4, 'type', 'body';
INSERT _InfoRg123 SELECT 5, 'type', 'body';
INSERT _InfoRg123 SELECT 6, 'type', 'body';
COMMIT TRANSACTION;
Шаг 3. Транзакция C: откроем новое окно панели запросов и выполним следующий код SQL. Этот код просто проверяет наличие сообщений в таблице очереди. Обратите внимание, что используется хинт NOLOCK, который позволяет выполнить “грязное” чтение и получить все записи таблицы, в том числе ещё незафиксированной транзакции A. Итогом выполнения скрипта будет таблица приведённая ниже. Скрипт выполнится моментально.
SELECT _Fld124 AS НомерСообщения,
_Fld125 AS ТипСообщения,
_Fld126 AS ТелоСообщения
FROM _InfoRg123 WITH (NOLOCK)
Результат выполнения скрипта
НомерСообщения | ТипСообщения | ТелоСообщения |
---|---|---|
1 | type | body |
2 | type | body |
3 | type | body |
4 | type | body |
5 | type | body |
6 | type | body |
Шаг 4. Транзакция D: откроем новое окно панели запросов и выполним следующий код SQL. Этот код эмулирует выполнение команды CONSUME без опции WITH STRICT ORDER
. Итогом выполнения скрипта будет таблица приведённая ниже. Скрипт выполнится моментально.
Важно! Нужно успеть выполнить скрипт до того, как завершиться транзакция A.
WITH queue AS
(SELECT TOP (10)
_Fld124 AS НомерСообщения,
_Fld125 AS ТипСообщения,
_Fld126 AS ТелоСообщения
FROM _InfoRg123 WITH (ROWLOCK, READPAST)
ORDER BY _Fld124 ASC)
DELETE queue
OUTPUT deleted.НомерСообщения,
deleted.ТипСообщения,
deleted.ТелоСообщения;
Результат выполнения скрипта
НомерСообщения | ТипСообщения | ТелоСообщения |
---|---|---|
4 | type | body |
5 | type | body |
6 | type | body |
Шаг 5. Прейдём на панель запросов транзакции A и дождёмся её успешного завершения с фиксацией транзакции.
Шаг 6. Прейдём на панель запросов транзакции D и выполним скрипт ещё раз, он выполнится моментально. Итогом выполнения будет ниже следующая таблица.
Результат выполнения скрипта
НомерСообщения | ТипСообщения | ТелоСообщения |
---|---|---|
1 | type | body |
2 | type | body |
3 | type | body |
Шаг 7. Выполним скрипт транзакции D ещё раз. Итогом его выполнения будет пустая таблица - очередь пуста. Мы обработали все сообщения.
Выводы
Итак, случилось нарушение последовательности сообщений. Сначала мы получили сообщения 4, 5 и 6, а только затем - 1, 2 и 3. Кто виноват и что делать?
На самом деле это штатное поведение команды CONSUME без использования опции WITH STRICT ORDER
и именно такое использование команды является рекомендованным.
Нарушаем ли мы таким образом последовательсность формирования сообщений? Безусловно да. Однако намного важнее другой вопрос: нарушаем ли мы таким образом согласованность данных в двух узлах обмена данными - источнике и приёмнике? Скорее всего нет и вот почему.
Регистрация изменения данных в регистр сведений исходящих сообщений чаще всего (рекомендовано) осуществляется при их записи в соответствующих подписках на события объектов метаданных 1С:Предприятие 8, то есть, во-первых, в транзакции, а, во-вторых, при удержании эксклюзивной блокировки на ключе такого объекта - ссылочного или набора записей.
Таким образом правильная регистрация изменений в сочетании с использованием команды CONSUME гарантирует последовательность доставки сообщений строго в порядке следования изменений объектов 1С по их ключам на уровне соответствующих механизмов СУБД. Эта очень важная оговорка содержится также в статье документации DaJet Script про управление последовательностью.
Здесь очень важно отметить и иметь ввиду при проектировании обменов данными, что, если используется какой-то иной способ регистрации изменений или формирование сообщений типа событие
, где нет привязки к ключу объекта или иному механизму сериализации транзакций, то следует придумать собственный механизм, гарантирующий последовательсность записи и порядок фиксации транзакций на уровне СУБД. В противном случае следует предполагать обработку записей таблицы-очереди в случайной последовательности, что, кстати сказать, не всегда является критичным или нежелательным.
Пример № 2 использование опции WITH STRICT ORDER
Повторяем шаги №№ 1-3 из первого примера выше. Модифицируем скрипт шага № 4: удаляем в коде SQL хинт READPAST
. Это будет соответствовать использованию опции WITH STRICT ORDER
команды CONSUME в коде DaJet Script.
WITH queue AS
(SELECT TOP (10)
_Fld124 AS НомерСообщения,
_Fld125 AS ТипСообщения,
_Fld126 AS ТелоСообщения
FROM _InfoRg123 WITH (ROWLOCK)
ORDER BY _Fld124 ASC)
DELETE queue
OUTPUT deleted.НомерСообщения,
deleted.ТипСообщения,
deleted.ТелоСообщения;
Выполняем модифицированный скрипт шага № 4 и наблюдаем “подвисание” панели запросов - выполняется ожидание завершения и фиксации транзакции A из шага № 1. Итогом выполнения будет ниже следующая таблица.
Результат выполнения скрипта
НомерСообщения | ТипСообщения | ТелоСообщения |
---|---|---|
1 | type | body |
2 | type | body |
3 | type | body |
4 | type | body |
5 | type | body |
6 | type | body |
Выводы
В отличие от первого примера таблица-очередь была обработана строго последовательно согласно номерам сообщений. Однако за это пришлось заплатить ожиданием на блокировках, созданных транзакцией A. Насколько такой сценарий использования команды CONSUME желателен или нет необходимо решать при проектировании обмена данными. Во всяком случае такая возможность есть.
В заключение этой статьи хотелось бы ещё отметить вот какой нюанс. Предположим используется типовой обмен на планах обмена. Интенсивность обменов достигла того предела, когда возникает характерная для этого проблема блокировок на таблицах регистрации изменений планов обмена. Решили перевести обмены на регистры сведений. Небольшое дополнение: есть два плана обмена. Проблема возникает, например, на объекте типа “Заказ клиента”. Этот объект включён в состав обоих планов обмена.
Нюанс: если отказаться только от одного плана обмена и перевести его на регистры сведений, то второй план обмена всё-равно может остаться источником проблем. Дело в том, что регистрация изменений объекта “Заказ клиента” в регистр сведений и в оставшийся план обмена выполняется в одной и той же транзакции!