Разработка крупных корпоративных систем на платформе 1С Предприятие часто сталкивается с проблемой актуализации данных на рабочих местах пользователей. Представим классическую ситуацию: более 200 менеджеров одновременно работают со списком заказов или заявок. В формах списков выводятся вычисляемые поля, остатки, статусы и другая критически важная информация. Если включить стандартное автообновление динамического списка (например, каждые 5 секунд), СУБД моментально получит колоссальную нагрузку — сотни тяжелых запросов в секунду. Это неизбежно приведет к замедлению работы интерфейса и блокировкам.
В этой статье мы подробно разберем, как решить задачу оперативного обновления списков у всех активных пользователей при изменении документов, не перегружая сервер. Мы рассмотрим как классический оптимизированный метод через служебный регистр сведений, так и современный бесконтактный способ с использованием Системы взаимодействия.
Стандартный механизм автообновления динамического списка просто перезапускает его основной запрос по таймеру. Если запрос содержит левые соединения, виртуальные таблицы или вычисляемые поля, сервер быстро исчерпает свои ресурсы.
Первая идея, которая приходит в голову разработчикам — записывать факт изменения документа в периодический регистр сведений (как лог), а на клиенте раз в несколько секунд считывать изменения через виртуальную таблицу СрезПоследних. Рассмотрим, почему это плохое решение для высоконагруженной системы:
СрезПоследних с условием по времени на постоянно растущем регистре создает значительную нагрузку на СУБД. При 200 клиентах, опрашивающих базу раз в 5 секунд, производительность системы резко упадет.Ниже мы рассмотрим два правильных архитектурных подхода к решению этой задачи.
Если в вашей системе не используется Система взаимодействия (или установлены жесткие ограничения совместимости платформы), лучшим решением будет использование облегченного непериодического регистра сведений. Его суть заключается в том, что мы храним не всю историю изменений (лог), а только текущую версию (или время последнего изменения) для каждого типа метаданных.
Проанализируем структуру такого регистра:
ВерсииТаблицДляОбновления (Непериодический, независимый).ИмяТаблицы (Тип: Строка, длина 150. Здесь будет храниться, например, "Документ.ЗаявкаПокупателя").Версия (Тип: Число, длина 15. Сюда мы будем записывать постоянно увеличивающийся счетчик или уникальный таймстемп в миллисекундах).При таком подходе в регистре будет ровно столько строк, сколько типов документов отслеживается в системе (обычно не более нескольких десятков). Запрос к такой таблице по первичному ключу выполняется СУБД мгновенно и полностью кэшируется.
Для того чтобы фиксировать изменения при любом способе записи (интерактивно пользователем, через фоновое задание или внешние Web-сервисы), регистрацию необходимо делать в подписке на событие ПриЗаписи для нужных документов.
Создадим общий модуль ОбновлениеСписковСервер и пропишем в нем процедуру регистрации:
Процедура ЗарегистрироватьИзменениеДанных(Источник, Отказ) Экспорт
// Получаем имя метаданных измененного объекта
ИмяТаблицы = Источник.Метаданные().ПолноеИмя();
// Записываем обновленную версию в регистр сведений
НаборЗаписей = РегистрыСведений.ВерсииТаблицДляОбновления.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.ИмяТаблицы.Установить(ИмяТаблицы);
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.ИмяТаблицы = ИмяТаблицы;
// Используем универсальное время в миллисекундах как счетчик версий
НоваяЗапись.Версия = ТекущаяУниверсальнаяДатаВМиллисекундах();
Попытка
НаборЗаписей.Записать(Истина);
Исключение
// Запись не должна блокировать проведение основного документа
ЗаписьЖурналаРегистрации("Обновление Списков", УровеньЖурналаРегистрации.Предупреждение, , , ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
КонецПроцедуры
Теперь настроим клиентскую сторону. Чтобы не вызывать сервер лишний раз, каждая форма списка будет хранить локально ту версию таблицы, которую она отображает в данный момент.
В модуле управляемой формы списка (например, списка заявок) реализуем следующий алгоритм:
&НаКлиенте
Перем ЛокальнаяВерсияТаблицы;
&НаКлиенте
Процедура ПриОткрытии(Отказ)
// Инициализируем локальную версию при открытии формы
ЛокальнаяВерсияТаблицы = 0;
// Подключаем обработчик ожидания, который будет проверять изменения каждые 5 секунд
ПодключитьОбработчикОжидания("ПроверитьОбновлениеСписка", 5);
КонецПроцедуры
&НаКлиенте
Процедура ПроверитьОбновлениеСписка()
// Вызываем облегченный серверный метод для проверки актуальности версии
ДанныеОбновления = ПроверитьВерсиюТаблицыНаСервере("Документ.ЗаявкаПокупателя", ЛокальнаяВерсияТаблицы);
Если ДанныеОбновления.ТребуетсяОбновление Тогда
// Запоминаем новую версию
ЛокальнаяВерсияТаблицы = ДанныеОбновления.АктуальнаяВерсия;
// Обновляем динамический список на форме
Элементы.Список.Обновить();
КонецЕсли;
КонецПроцедуры
В модуле формы или специализированном общем модуле с признаком "Вызов сервера" опишем функцию сравнения версий:
&НаСервереБезКонтекста
Функция ПроверитьВерсиюТаблицыНаСервере(ИмяТаблицы, ТекущаяЛокальнаяВерсия) Экспорт
Результат = Новый Структура("ТребуетсяОбновление, АктуальнаяВерсия", Ложь, ТекущаяЛокальнаяВерсия);
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ВерсииТаблицДляОбновления.Версия КАК Версия
|ИЗ
| РегистрСведений.ВерсииТаблицДляОбновления КАК ВерсииТаблицДляОбновления
|ГДЕ
| ВерсииТаблицДляОбновления.ИмяТаблицы = &ИмяТаблицы";
Запрос.УстановитьПараметр("ИмяТаблицы", ИмяТаблицы);
УстановитьПривилегированныйРежим(Истина); // Оптимизируем доступ без проверок прав RLS к этой таблице
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Если Выборка.Версия > ТекущаяЛокальнаяВерсия Тогда
Результат.ТребуетсяОбновление = Истина;
Результат.АктуальнаяВерсия = Выборка.Версия;
КонецЕсли;
КонецЕсли;
Возврат Результат;
КонецФункции
Преимущества данного метода: Запрос выполняется к плоской таблице по индексу первичного ключа. СУБД обрабатывает такой запрос за доли миллисекунды. Даже 200 пользователей не создадут заметной нагрузки на сервер баз данных.
Если ваша конфигурация работает на платформе версии 8.3.11 и выше, а в системе развернут Сервер взаимодействия, вы можете полностью отказаться от опроса базы данных по таймеру. Данный механизм использует постоянное сетевое подключение по протоколу WebSockets. Сервер 1С сам "протолкнет" (Push) событие об изменении данных на все активные клиенты.
Разберем по шагам, как настроить такую рассылку уведомлений.
В подписке на событие ПриЗаписи регистрируем изменение и отправляем сообщение в специальное неконтекстное обсуждение:
Процедура ОтправитьОповещениеВСистемуВзаимодействия(Источник, Отказ) Экспорт
ИмяТаблицы = Источник.Метаданные().ПолноеИмя();
// Получаем или создаем служебное обсуждение для обновлений интерфейса
УстановитьПривилегированныйРежим(Истина);
КлючОбсуждения = "СлужебноеОбновлениеФорм";
Обсуждение = СистемаВзаимодействия.ПолучитьОбсуждениеПоКлючу(КлючОбсуждения);
Если Обсуждение = Неопределено Тогда
ПараметрыОбсуждения = Новый ПараметрыВыбораОбсуждения;
ПараметрыОбсуждения.Ключ = КлючОбсуждения;
ПараметрыОбсуждения.ОтображаемоеИмя = "Обновление динамических списков";
Обсуждение = СистемаВзаимодействия.СоздатьОбсуждение(ПараметрыОбсуждения);
КонецЕсли;
// Отправляем сообщение, где текстом является имя изменившейся таблицы
Сообщение = СистемаВзаимодействия.СоздатьСообщение(Обсуждение.Идентификатор);
Сообщение.Текст = ИмяТаблицы;
Сообщение.Записать();
КонецПроцедуры
В модуле управляемого приложения (или при начале работы системы) необходимо подключить обработчик новых сообщений Системы взаимодействия. Это позволит централизованно принимать сигналы от сервера.
&НаКлиенте
Процедура ПриНачалеРаботыСистемы()
// Инициализируем прослушивание сообщений
ИнициализироватьПодпискуНаОбновления();
КонецПроцедуры
&НаКлиенте
Процедура ИнициализироватьПодпискуНаОбновления()
// Подключаем обработчик сообщений Системы взаимодействия
СистемаВзаимодействия.ПодключитьОбработчикНовыхСообщений(
Новый ОписаниеОповещения("ПриПолученииНовогоСообщенияСистемыВзаимодействия", ЭтотОбъект)
);
КонецПроцедуры
&НаКлиенте
Процедура ПриПолученииНовогоСообщенияСистемыВзаимодействия(Сообщение, ДополнительныеПараметры) Экспорт
// Проверяем, что сообщение пришло из нашего служебного обсуждения
Если Сообщение.Обсуждение.Ключ = "СлужебноеОбновлениеФорм" Тогда
// Рассылаем широковещательное оповещение внутри текущего сеанса клиента
// В качестве параметра передаем имя изменившейся таблицы
Оповестить("ТаблицаИзменена", Сообщение.Текст);
КонецЕсли;
КонецПроцедуры
Теперь в форме списка динамического документа достаточно настроить стандартную процедуру ОбработкаОповещения:
&НаКлиенте
Процедура ОбработкаОповещения(ИмяСобытия, Параметр, Источник)
// Если пришло оповещение о том, что изменилась интересующая нас таблица
Если ИмяСобытия = "ТаблицаИзменена" И Параметр = "Документ.ЗаявкаПокупателя" Тогда
// Мгновенно обновляем отображение данных на форме
Элементы.Список.Обновить();
КонецЕсли;
КонецПроцедуры
Почему это лучшее решение? База данных вообще не участвует в процессе опроса. Клиентские приложения находятся в режиме ожидания. Нагрузка на сервер 1С и СУБД сводится к абсолютному нулю, а обновление происходит мгновенно в момент записи документа.
Разработчики часто совершают ошибку, размещая код вызова обновлений в событиях экранных форм, например, в ОбработкаПроверкиЗаполнения или процедурах интерактивного закрытия формы.
Помните, что в современных реалиях документы могут создаваться и изменяться внешними системами:
При таких сценариях интерактивные события форм не вызываются. Чтобы ваши пользователи гарантированно видели актуальные данные при любых изменениях, всегда размещайте вызов процедур оповещения (будь то запись в служебный регистр или отправка сообщения в Систему взаимодействия) исключительно в модулях объектов (в событии ПриЗаписи) или в соответствующих Подписках на события.
Мы подробно разобрали два эффективных сценария настройки уведомлений для динамических списков: