В процессе разработки на платформе 1С:Предприятие 8 часто возникает задача регистрации быстрых событий. Это может быть логирование отправки и получения HTTP-запросов, фиксация этапов выполнения длительных фоновых процессов или запись пакетов данных интеграции. На первый взгляд задача кажется тривиальной: создаем периодический регистр сведений, пишем туда события через менеджер записи или набор записей. Однако на практике разработчики часто сталкиваются с загадочным поведением системы: часть записей бесследно исчезает из базы данных.
Особенно сильно сбивает с толку так называемый эффект отладчика. Когда мы запускаем код в обычном режиме, в регистре сохраняется лишь часть логов (например, из четырех записей создаются только две). Но стоит нам включить пошаговую отладку и пройти тот же самый код вручную — все записи успешно сохраняются в регистре. В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем внутренние механизмы платформы 1С и рассмотрим лучшие архитектурные практики для решения подобных задач.
Давайте проанализируем ситуацию на примере кода, предоставленного автором темы на форуме. Код выглядит следующим образом:
МенеджерЗаписи = РегистрыСведений.ЦеныНаТопливо.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Период = ТекущаяДата();
МенеджерЗаписи.ВидТоплива = ВидТоплива;
МенеджерЗаписи.ПоставщикТоплива = ПоставщикТоплива;
МенеджерЗаписи.Записать();
Для периодического регистра сведений уникальность каждой записи (ее ключ) определяется строго по комбинации: Период + Измерения. В рассматриваемом примере измерениями являются Период и ВидТоплива. Поле ПоставщикТоплива, судя по всему, является ресурсом или реквизитом.
Рассмотрим по шагам, что происходит при выполнении этого кода на высокой скорости:
ТекущаяДата() возвращает системное время с точностью до секунды. Миллисекунды этой функцией не учитываются.МенеджерЗаписи.Записать(), платформа 1С выполняет транзакционное действие в СУБД. Она проверяет, существует ли уже в базе данных запись с таким ключом (совпадающим периодом и измерениями).
В результате, если мы отправляем 4 запроса в течение одной секунды по одному и тому же виду топлива, в базе данных останется только одна запись — последняя. Предыдущие три будут последовательно затерты при перезаписи ключа.
Разница в поведении кода при отладке и в режиме реальной работы — классический симптом рассинхронизации по времени. Когда разработчик идет по коду пошагово (нажимая клавишу F10 в конфигураторе), между выполнением строк кода проходит физическое время. На анализ одной строки уходит как минимум одна-две секунды. За это время системные часы успевают сдвинуться вперед. Функция ТекущаяДата() на каждом шаге возвращает новое уникальное значение секунды. Ключ записи каждый раз получается уникальным, и перезаписи не происходит. В обычном же режиме код выполняется за миллисекунды, и даты совпадают.
Для того чтобы логирование работало корректно и данные не затирались, нам необходимо обеспечить гарантированную уникальность ключа записи при высокой интенсивности событий. Рассмотрим четыре основных способа реализации этой задачи.
Это самый простой, надежный и архитектурно правильный способ обеспечения уникальности записей. Мы добавляем в регистр сведений новое измерение, которое будет уникальным для каждого события.
Пошаговый алгоритм:
ИдентификаторЗаписи с типом УникальныйИдентификатор (или Строка длиной 36 символов, если требуется совместимость со старыми конфигурациями).Посмотрим на пример измененного кода:
НовыйЛог = РегистрыСведений.ЦеныНаТопливо.СоздатьМенеджерЗаписи();
НовыйЛог.Период = ТекущаяДата();
НовыйЛог.ВидТоплива = ВидТоплива;
НовыйЛог.ИдентификаторЗаписи = Новый УникальныйИдентификатор(); // Гарантия уникальности
НовыйЛог.ПоставщикТоплива = ПоставщикТоплива;
НовыйЛог.Записать();
Благодаря этому решению, даже если за одну секунду будет зафиксировано 100 событий по одному виду топлива, каждое из них получит уникальный GUID в ключе записи, и все логи успешно сохранятся в базе данных.
Если для бизнес-логики крайне важно сохранять точную хронологическую последовательность записей с точностью выше одной секунды, стандартный период регистра нам не поможет. Начиная с версии платформы 1С 8.3.17, разработчикам доступна функция работы с миллисекундами.
Пошаговый алгоритм:
ВремяВМиллисекундах (тип Число, длина 20, точность 0).ТекущаяУниверсальнаяДатаВМиллисекундах().Рассмотрим пример кода:
МенеджерЗаписи = РегистрыСведений.ЦеныНаТопливо.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Период = ТекущаяДата();
МенеджерЗаписи.ВидТоплива = ВидТоплива;
МенеджерЗаписи.ВремяВМиллисекундах = ТекущаяУниверсальнаяДатаВМиллисекундах();
МенеджерЗаписи.ПоставщикТоплива = ПоставщикТоплива;
МенеджерЗаписи.Записать();
Важное примечание: На сверхбыстрых серверах или при многопоточной обработке данных даже миллисекунды теоретически могут совпасть. Поэтому для стопроцентной гарантии уникальности этот метод рекомендуется комбинировать с добавлением автоинкрементного счетчика или уникального идентификатора.
Если для регистра сведений не требуется получать "срез последних" на определенную дату (что обычно и бывает при логировании), использование периодического регистра сведений избыточно. Гораздо правильнее спроектировать регистр как непериодический.
При такой структуре:
Идентификатор (тип УникальныйИдентификатор).ДатаСобытия с типом Дата).При этом платформа 1С не будет контролировать уникальность записей по времени, индекс таблицы СУБД станет более компактным, а запись логов будет происходить быстрее.
Запись логов HTTP-запросов и интеграционных пакетов непосредственно в транзакционную базу данных 1С таит в себе серьезные опасности, о которых должен знать каждый разработчик.
МенеджерЗаписи для частой фиксации событий создает значительную нагрузку. При вызове метода Записать() платформа сначала выполняет неявный запрос SELECT к базе данных для проверки существования записи, а затем INSERT или UPDATE. В условиях многопоточности это приводит к частым эскалациям блокировок таблиц СУБД, замедляя работу основных пользователей.Для снижения нагрузки на основную базу данных рекомендуется использовать альтернативные подходы:
ЗаписатьСобытиеЖурналаРегистрации(). Журнал регистрации пишется платформой асинхронно, вне транзакции базы данных СУБД. Даже при откате транзакции запись в журнале регистрации сохранится.