Счётчик в 1С 8.3: от хранилища значения до выдачи номера
Содержание
Счётчик в 1С 8 — это не отдельный объект конфигурации, а приём: вы храните последнее выданное значение и при каждом запросе увеличиваете его на единицу. Самая частая ошибка новичка — держать это число в реквизите справочника или в константе и читать-писать его без блокировки. При двух одновременных сеансах оба прочитают одно и то же значение и выдадут одинаковый номер.
Правильно хранить значение в независимом регистре сведений, а следующий номер получать внутри транзакции с управляемой блокировкой. Тогда второй сеанс ждёт, пока первый допишет своё число. Если же вам нужна просто сквозная нумерация документов, самодельный счётчик вообще не нужен — хватит штатного нумератора платформы.
Ниже — где хранить значение, рабочий код выдачи номера, настройка нумерации документов, обработка ошибок, блокировки под нагрузкой и хранение истории значений.
Где хранить значение счётчика
Хранилище определяет, переживёт ли счётчик параллельную работу пользователей. Вариантов три.
🔹 Константа — одно глобальное число. Подходит для файловой базы и одного счётчика на всю конфигурацию. Минус: под каждый новый счётчик нужна новая константа, а защита от одновременной записи всё равно ложится на вас.
🔹 Реквизит справочника или документа — худший вариант. Нет ни блокировки на уровне строки, ни удобного ключа. Для счётчика это тупик: дубли гарантированы под многопользовательской работой.
🔹 Независимый регистр сведений — рекомендуемый способ. Один регистр обслуживает сколько угодно счётчиков: разрезы задаются измерениями (организация, склад, год, тип документа), само значение лежит в ресурсе. Запись защищается управляемой блокировкой по ключу.
| Хранилище | Несколько счётчиков | Защита от дублей | История значений |
|---|---|---|---|
| Константа | нет, по одной на счётчик | вручную через блокировку | нет |
| Реквизит объекта | нет | фактически нет | нет |
| Регистр сведений | да, через измерения | блокировка по ключу | да, если периодический |
Для большинства задач создайте независимый непериодический регистр сведений, например СчётчикиНумерации:
- измерение
Организация(и при необходимостиГод,Префикс,ТипОперации); - ресурс
ПоследнийНомертипаЧисло, достаточной длины и с нулём знаков после запятой.
Если история значений не нужна, периодичность регистра ставьте «Непериодический» — так меньше данных и быстрее запись.
Как получить следующий номер без дублей
Ключевая часть — атомарно прочитать текущее значение, увеличить его и записать обратно, не дав другому сеансу вклиниться между чтением и записью. Для этого открывают транзакцию и ставят исключительную управляемую блокировку на ключ регистра до чтения.
Блокировку ставьте ДО чтения значения, а не после. Если прочитать значение, а потом блокировать, второй сеанс успеет прочитать то же число — смысл блокировки теряется.
Функция ПолучитьСледующийНомер(Организация) Экспорт
Результат = 0;
НачатьТранзакцию();
Попытка
// 1. Блокируем ключ регистра до чтения
Блокировка = Новый БлокировкаДанных;
Элемент = Блокировка.Добавить("РегистрСведений.СчётчикиНумерации");
Элемент.Режим = РежимБлокировкиДанных.Исключительный;
Элемент.УстановитьЗначение("Организация", Организация);
Блокировка.Заблокировать();
// 2. Читаем текущее значение
МенеджерЗаписи = РегистрыСведений.СчётчикиНумерации.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Организация = Организация;
МенеджерЗаписи.Прочитать();
// 3. Увеличиваем. Если записи ещё не было — начинаем с единицы
Результат = ?(МенеджерЗаписи.Выбран(), МенеджерЗаписи.ПоследнийНомер, 0) + 1;
// 4. Записываем обратно (Прочитать при неудаче очищает поля — задаём заново)
МенеджерЗаписи.Организация = Организация;
МенеджерЗаписи.ПоследнийНомер = Результат;
МенеджерЗаписи.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Возврат Результат;
КонецФункции
Функцию размещают в общем модуле с флагом «Сервер» и вызывают из обработчика записи документа или из команды. Благодаря блокировке по значению измерения Организация счётчики разных организаций друг друга не тормозят — ждут только те сеансы, что претендуют на один и тот же ключ.
Отдельный вопрос — откуда вызывать функцию. Если счётчик привязан к документу, номер выдают в обработчике ПередЗаписью или ОбработкаПроведения серверного модуля объекта. Из клиентского кода напрямую к регистру обращаться нельзя — выдача номера это серверная операция. Менеджер записи здесь удобнее набора записей: он работает с одной строкой по полному ключу, а не выбирает коллекцию, и читается быстрее.
💡 Если нужно собрать строковый номер с префиксом и ведущими нулями, число форматируют отдельно: СтрШаблон("%1-%2", Префикс, Формат(Результат, "ЧЦ=6; ЧВН=; ЧГ=0")). Склеивать число со строкой без Формат нельзя — получите ошибку преобразования.
Перед запуском в работу проверьте реализацию по короткому списку:
Нумерация документов штатными средствами
Если задача звучит как «сделать сквозную нумерацию документов», самописный счётчик чаще всего избыточен. Платформа 8.3 нумерует документы сама — достаточно настроить это в конфигураторе.
Главный приём, который часто пропускают: общий нумератор. Создайте объект «Нумератор» и назначьте его нескольким документам — тогда они будут нумероваться в единой сквозной последовательности, а не каждый по себе.
В свойствах документа или нумератора задают:
- Тип номера —
ЧислоилиСтрока. Для префиксов берут строковый. - Длина номера — сколько знаков резервируется.
- Периодичность —
В пределах года,квартала,месяца,дняилиНепериодический. От неё зависит, когда нумерация обнуляется. - Автонумерация — платформа сама подставит следующий номер при записи нового документа.
- Проверка уникальности — не даст записать документ с занятым номером.
Чтобы номер обнулялся с 1 января, поставьте периодичность В пределах года — отдельный счётчик для этого не нужен.
Собственный префикс подставляют в обработчике события документа ПриУстановкеНовогоНомера:
Процедура ПриУстановкеНовогоНомера(СтандартнаяОбработка, Префикс)
Префикс = "ЗАК-";
КонецПроцедуры
Здесь СтандартнаяОбработка оставляют включённой — тогда платформа добавит к вашему префиксу свой порядковый номер. Этого хватает для большинства сценариев: приказы, заказы, акты с буквенным кодом подразделения.
С длиной номера есть тонкость. Для строкового типа в длину входят и символы префикса, поэтому короткое поле быстро «переполнится» на сквозной нумерации — закладывайте запас. Для числового типа при переполнении платформа начнёт выдавать номера с большим числом знаков, а проверка уникальности не даст записать документ с занятым номером, если вы правили нумерацию вручную. Если префикс зависит от реквизитов самого документа (например, от подразделения), в обработчике ПриУстановкеНовогоНомера доступен объект через ЭтотОбъект — префикс можно собрать на лету.
Самописный счётчик из прошлого раздела нужен только там, где штатной нумерации мало: отдельная сквозная нумерация внутри табличной части, номера, не привязанные к объекту, или своя логика сброса.
Обработка ошибок и повторных попыток
Блок с транзакцией уязвим к двум ситуациям: исключение внутри (нарушена логика, упала запись) и тайм-аут блокировки, когда соседний сеанс держит ключ дольше допустимого.
Исключение внутри транзакции обязательно сопровождают откатом. В коде выше это делает ветка Исключение с ОтменитьТранзакцию() и ВызватьИсключение. Без ОтменитьТранзакцию() транзакция остаётся незавершённой, и следующая попытка записи упадёт с ошибкой «В данной транзакции уже происходили ошибки».
Тайм-аут блокировки по умолчанию — 20 секунд. Если ключ занят дольше, платформа возбуждает исключение. Для коротких операций выдачи номера это редкость, но под нагрузкой разумно сделать несколько повторов:
Попытка = 0;
Пока Попытка < 3 Цикл
Попытка = Попытка + 1;
Попытка // ключевое слово начала обработки исключения
Номер = ПолучитьСледующийНомер(Организация);
Прервать;
Исключение
Если Попытка = 3 Тогда
ВызватьИсключение;
КонецЕсли;
КонецПопытки;
КонецЦикла;
Повтор имеет смысл только для ошибок блокировки и взаимоблокировок (deadlock) — их платформа «лечит» самим фактом повторного запуска транзакции. Логические ошибки повтором не исправить, поэтому в реальном коде тип исключения стоит различать по тексту через ИнформацияОбОшибке().
Не ловите исключение «молча». Если после трёх попыток номер не выдан, пользователь должен увидеть понятное сообщение, а не записать документ с нулём вместо номера.
Производительность и блокировки под нагрузкой
Счётчик — узкое место по определению: все, кто выдаёт номер по одному ключу, выстраиваются в очередь. Задача — сократить время удержания блокировки.
🔹 Блокируйте как можно позже. Ставьте блокировку непосредственно перед чтением значения, а не в начале длинной процедуры. Чем меньше кода между Заблокировать() и ЗафиксироватьТранзакцию(), тем короче очередь.
🔹 Выдавайте номер в конце записи документа. Если получить номер в самом начале проведения, блокировка счётчика будет держаться всё время, пока формируются движения по регистрам. Лучше получить номер последним действием.
🔹 Дробите счётчики измерениями. Отдельные ключи по организациям, складам или типам документов означают независимые очереди. Сеансы разных организаций не ждут друг друга.
🔹 Используйте управляемый режим блокировок. В свойствах конфигурации режим управления блокировкой данных должен быть Управляемый. Исключительная управляемая блокировка по конкретному ключу гораздо «уже» автоматической блокировки всей таблицы.
🔹 Не делайте в транзакции лишнего. Запросы к другим таблицам, запись логов, вызовы внешних систем — всё это удлиняет удержание ключа. Вынесите их за пределы блока выдачи номера.
Взаимоблокировки (deadlock) возникают, когда два процесса захватывают ресурсы в разном порядке. Для счётчиков это редкость, если один ключ блокируется один раз, но если в одной транзакции вы трогаете несколько счётчиков — блокируйте их всегда в одинаковом порядке.
Хранение истории значений счётчика
Иногда нужно знать не только текущее значение, но и каким оно было на конкретную дату — например, чтобы понять, сколько номеров выдали за месяц, или восстановить картину при разборе дублей.
Для этого регистр сведений делают периодическим. Тогда каждая запись привязана к полю Период, и платформа сама хранит значения по датам. Текущее значение на нужную дату берут срезом последних:
Отбор = Новый Структура("Организация", Организация);
Запись = РегистрыСведений.СчётчикиНумерации.СрезПоследних(ТекущаяДата(), Отбор);
Минус периодического регистра — он тяжелее непериодического: записей больше, запись чуть медленнее. Поэтому периодичность включают осознанно, когда история действительно нужна, а не «на всякий случай».
Если же нужен не журнал значений счётчика, а история изменений самих документов (кто и когда поменял номер или реквизиты), это уже другая задача — её закрывает механизм версионирования объектов из библиотеки стандартных подсистем, а не регистр счётчика.
Частые ошибки при реализации счётчика
Проблемы со счётчиком почти всегда сводятся к нескольким типовым промахам в проектировании — не в коде конкретной строки, а в подходе.
- 🔸 Разделяемая блокировка вместо исключительной. В режиме
Разделяемыйдва сеанса читают значение одновременно и получают один номер. Для счётчика нужен толькоИсключительный. - 🔸 Чтение без транзакции. Блокировка живёт только внутри транзакции. Без
НачатьТранзакцию()вызовЗаблокировать()ничего не защитит. - 🔸 Хранение в реквизите или в видимой форме. Значение, доступное пользователю на редактирование, рано или поздно собьют вручную.
- 🔸 Выдача номера до проверки документа. Если номер получен, а запись документа потом отменяется, в последовательности появляется «дыра». Для многих задач это допустимо, но закладывать логику на отсутствие пропусков нельзя.
Отдельно про пропуски в нумерации: штатная автонумерация документов тоже резервирует номер при создании, и если документ не записать, номер пропадёт. Строгая нумерация «без единого пропуска» средствами платформы не гарантируется — закладывайтесь на это заранее.
Частые вопросы
Можно ли сделать счётчик на константе?
Как обнулить счётчик в начале года?
Год — при смене года ключ станет другим и отсчёт начнётся с единицы.