Использование планов обмена зачастую приводит к блокировкам, взаимоблокировкам, дедлокам. Здесь можно найти немало хороших публикаций, объясняющих суть этих блокировок, например, вот (список других в конце статьи), а также публикаций, предлагающих переходить с планов обмена на альтернативные варианты - регистры сведений, историю данных и прочие варианты. Наверное, в запущенных случаях без отказа не обойтись, но что делать, если у вас внедрены планы обмена, и очень не хочется тратить несколько месяцев на переход на другое решение? Попробую рассказать, как мы боролись с блокировками и что из этого вышло.
Итак, допустим, у вас имеется база, в которой активно работает сотня пользователей. Я упрощу данные, с которыми они могли бы работать, например, ведется учет каких-то слушателей. Есть справочник Слушатели. И у них есть какая-то история, там, зачислили, отчислили, выдали диплом, соответственно потребуется регистр сведений СтатусыСлушателей. А еще у них могут в процессе меняться какие-то реквизиты, прочие данные, соответственно, будет еще один регистр ДанныеСлушателей. Не спрашивайте, почему это два регистра, а не один, я попытался на пальцах нарисовать логику, в реальном проекте каких-то связанных с физическим лицом регистров вообще может быть пачка. Все данные регистров меняются документами, проведение создает запись, отмена удаляет.
Кроме того, база должна обмениваться данными с чем-то еще, например, с сайтом (да, можно реализовать парсинг сообщений плана обмена вне 1с, но это совсем другая история про то, как бы повторили функциональность планов обмена вне 1с и поддерживаем формат совместимый с 1с). Раньше с сайтом обменивались по ночам, теперь решили, что важна актуальность, и поставили обмен каждую минуту (но зато меньше данных на сообщение).
И вот начинаются проблемы с блокировками. Ну ладно там фоновое задание по обмену не отработало, отработает через минуту. Но у пользователей странные провисания на 20 секунд, а потом то удается провести документ, то лезет ругань. Неприятно. Когда ситуация становится массовой, приходится разбираться.
Но, как сказал бы Боромир, ... ну вы сами поняли. Волшебной кнопки нет, и вообще стопроцентного решения с планами обмена нет, хотя зачем нам 100 процентов. Все-таки не высокопроизводительные вычисления, где одна ошибка приводит к потере работы за несколько дней. Иногда можно и повисеть на блокировке немного, только надо, чтобы это было крайне редким событием. В общем, поехали.
Я не буду здесь в деталях пересказывать уже процитированную статью, но если кратко, то любимый метод плана обмена ВыбратьИзменения() устанавливает блокировку на все измененные данные. Как и почему? Ну он пробегается по всем таблицам изменений и бежит в них делать Update, заменяя Null на текущий номер сообщения. Обычная же запись в справочник или регистр, включенный в состав плана обмена, тоже пытается сделать Update по соответствующей записи в таблице изменений, прописав туда Null. Ну а теперь представим, что с данными идет частая работа, документ, например, провели, отменяем сразу по какой-то причине, то есть оба регистра СтатусыСлушателей и ДанныеСлушателей помечены как измененные. Начинается снова проведение и примерно одновременно фоновое задание вызвало ВыбратьИзменения(). Но в проведении мы сначала пишем в Статусы, а потом в Данные, а ВыбратьИзменения() обходит таблицы в обратном порядке. Вот вам и дедлок. Да, можно узнать, в каком порядке ВыбратьИзменения() обходит таблицы и писать при проведении в том же, но у нас вырожденный пример, а в реальной базе с кучей связанных справочников и регистров вы наверняка не сможете в каждой транзакции писать данные ровно в том порядке, в котором это делает план обмена.
Я не сказал, но, конечно, речь идет об управляемых блокировках. Что нам в данном случае советует 1с? Да, конечно, ставить платформенные управляемые блокировки. Например, при проведении, мы можем сделать так (здесь регистры независимые, у них есть измерение Слушатель).
БлокировкаДанных = Новый БлокировкаДанных;
ЭлементБлокировки = БлокировкаДанных.Добавить("РегистрСведений.ДанныеСлушателей");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.ИсточникДанных = Ссылка.Список;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Слушатель", "Слушатель");
ЭлементБлокировки = БлокировкаДанных.Добавить("РегистрСведений.СтатусыСлушателей");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.ИсточникДанных = Ссылка.Список;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Слушатель", "Слушатель");
БлокировкаДанных.Заблокировать();
Вставляем этот код, думаем, что теперь-то мы заблокировали оба регистра, и дедлока не будет... и это не помогает.
Здесь хочется сформулировать мысль, которая при чтении документации была не очевидной. Да, там написано про платформенную блокировку (это вот которая БлокировкаДанных.Заблокировать()) а также про блокировку СУБД (которую накладывает движок базы данных по операциям Update). Так вот почему-то было ощущение, что платформенные блокировки накладываются средствами блокировок СУБД (сейчас специалисты по оптимизации тихонько хихикают, ну что же). Так вот нет, совсем нет, и, надеюсь, что эта мысль будет важна для начинающих читателей. Платформенные блокировки и блокировки СУБД никак не связаны! Если почитать технологических журнал, у платформенных блокировок будет специальное событие TLock. Ну а СУБД, это вам, скорей, в анализатор событий СУБД.
Поэтому мало накладывать платформенную блокировку при проведении, надо тогда и перед ВыбратьИзменения(). Вопрос только, на что ее накладывать? Ну, например, можно сделать вот так:
Запрос = Новый Запрос;
Запрос.Текст ="ВЫБРАТЬ
| ДанныеСлушателейИзменения.Слушатель КАК Слушатель
|ИЗ
| РегистрСведений.ДанныеСлушателей.Изменения КАК ДанныеСлушателейИзменения";
ЭлементБлокировки = БлокировкаДанных.Добавить("РегистрСведений.ДанныеСлушателей");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Разделяемый;
ЭлементБлокировки.ИсточникДанных = Запрос.Выполнить().Выгрузить();
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Слушатель", "Слушатель");
Смотрите, что происходит. До вызова ВыбратьИзменения() мы обратились запросом к таблице изменений (что не накладывает никаких блокировок), взяли все, что изменилось, и дальше накладываем блокировку по измерению. Достаточно брать разделяемую, поскольку документы накладывают исключительную.
Только вот я сейчас на один регистр наложил блокировку. А, может, лучше на оба? Или, может, на все, что могло поменяться? Можно и на все, вот ниже работающий метакод:
Для Каждого Элемент Из Метаданные.ПланыОбмена.ОбменССайтом.Состав Цикл
Если СтрНачинаетсяС(Элемент.Метаданные.ПолноеИмя(), "Справочник.") Или
СтрНачинаетсяС(Элемент.Метаданные.ПолноеИмя(), "Документ.") Или
СтрНачинаетсяС(Элемент.Метаданные.ПолноеИмя(), "ПланВидовХарактеристик.") Тогда
// ссылочный тип
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| ОбъектИзменения.Ссылка КАК Ссылка
|ИЗ
| " + Элемент.Метаданные.ПолноеИмя() + ".Изменения КАК ОбъектИзменения";
ЭлементБлокировки = БлокировкаДанных.Добавить(Элемент.Метаданные.ПолноеИмя());
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Разделяемый;
ЭлементБлокировки.ИсточникДанных = Запрос.Выполнить().Выгрузить();
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Ссылка", "Ссылка");
ИначеЕсли
СтрНачинаетсяС(Элемент.Метаданные.ПолноеИмя(), "РегистрНакопления.") Или
СтрНачинаетсяС(Элемент.Метаданные.ПолноеИмя(), "РегистрСведений.") И Элемент.Метаданные.РежимЗаписи = Метаданные.СвойстваОбъектов.РежимЗаписиРегистра.ПодчинениеРегистратору Тогда
// блокировка по регистратору
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ РАЗЛИЧНЫЕ
| РегистрИзменения.Регистратор КАК Регистратор
|ИЗ
| " + Элемент.Метаданные.ПолноеИмя() + ".Изменения КАК РегистрИзменения";
ЭлементБлокировки = БлокировкаДанных.Добавить(Элемент.Метаданные.ПолноеИмя() + ".НаборЗаписей");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Разделяемый;
ЭлементБлокировки.ИсточникДанных = Запрос.Выполнить().Выгрузить();
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Регистратор", "Регистратор");
ИначеЕсли СтрНачинаетсяС(Элемент.Метаданные.ПолноеИмя(), "РегистрСведений.") Тогда
// блокировка по первому измерению
Для Каждого Измерение Из Элемент.Метаданные.Измерения Цикл
ИмяИзмерения = Измерение.Имя;
Прервать;
КонецЦикла;
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ РАЗЛИЧНЫЕ
| РегистрИзменения." + ИмяИзмерения + " КАК " + ИмяИзмерения + "
|ИЗ
| " + Элемент.Метаданные.ПолноеИмя() + ".Изменения КАК РегистрИзменения";
ЭлементБлокировки = БлокировкаДанных.Добавить(Элемент.Метаданные.ПолноеИмя());
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Разделяемый;
ЭлементБлокировки.ИсточникДанных = Запрос.Выполнить().Выгрузить();
ЭлементБлокировки.ИспользоватьИзИсточникаДанных(ИмяИзмерения, ИмяИзмерения);
КонецЕсли;
КонецЦикла;
Код рабочий, только поменяйте имя плана обмена. Ну и я не рассматривал регистры бухгалтерии, поскольку у нас их нет. Только вот здесь я вижу две сложности
Во-первых, надо ведь аккуратно выбирать, какие ставить блокировки. Если со ссылочными объектами в большинстве случаев понятно, основным пространством блокировки является Ссылка, то с регистрами есть варианты, можно блокировать по Измерениям, а у подчиненных регистров есть еще и НаборЗаписей по регистратору. Здесь я так и выбрал по регистратору для подчиненного, а для для независимых сделал блокировку по значению первого измерения. Возможно, вам не подойдет первое измерение, нужно, чтобы эта блокировка совпадала с тем, что используется в других операциях, поэтому смотрите.
Во-вторых, и это, на мой взгляд, куда более существенно, вы выполняете последовательность запросов, и это нагрузка на систему, особенно, если накапливается много данных для обмена. И, что еще более важно, имеется неустранимая проблема, заключающаяся в том, что между выполнением этих запросов и вызовом метода ВыбратьИзменения() что-то еще может быть изменено другим процессом, поэтому вы не можете гарантировать, что вы наложите платформенные блокировки на все, что будет блокироваться на изменение (в таблицах изменений) при помощи блокировок СУБД. Нет, можете, конечно, если серьезно расширить блокировки, но так вы вообще парализуете работу системы.
Чем это плохо? Ну тем, что не исключается ситуация, когда вы уже выполнили запрос к таблице изменений, затем кто-то завершает транзакцию с изменением чего-то, затем начинает новую, и только после этого дело доходит до ВыбратьИзменения(). Тогда этот метод не сработает. Поэтому нужно, с одной стороны, проследить свой код, чтобы он на одно действие пользователя держал по возможности одну транзакцию, меняющую одни данные, а, с другой стороны, сделать так, чтобы он первого запроса к таблице измененных до ВыбратьИзменения() проходил минимум времени, чтобы реактивный пользователь не мог вклиниться.
Так что в нашем случае мы отказались от мета-подхода и поставили блокировки перед ВыбратьИзменения() на ключевые регистры, с которыми должно быть пересечение с операциями над документами. И помогло, дедлоки сразу же прекратились.
И напоследок статьи на тему, что штудировались чтобы решить проблему (правда, удалось решить только после переписки в комментариях)
- Планы обмена. Управляемый режим блокировок
- Тюнинг планов обмена
- Планы обмена 1С
- Планы обмена VS История данных
- Планы обмена. Квитировать или гарантировать?
- Анализ блокировок СУБД: таблица изменений плана обмена 1С