Счётчик в 1С 8.3: от хранилища значения до выдачи номера

· 9 мин

Содержание

Счётчик в 1С 8 — это не отдельный объект конфигурации, а приём: вы храните последнее выданное значение и при каждом запросе увеличиваете его на единицу. Самая частая ошибка новичка — держать это число в реквизите справочника или в константе и читать-писать его без блокировки. При двух одновременных сеансах оба прочитают одно и то же значение и выдадут одинаковый номер.

Правильно хранить значение в независимом регистре сведений, а следующий номер получать внутри транзакции с управляемой блокировкой. Тогда второй сеанс ждёт, пока первый допишет своё число. Если же вам нужна просто сквозная нумерация документов, самодельный счётчик вообще не нужен — хватит штатного нумератора платформы.

Ниже — где хранить значение, рабочий код выдачи номера, настройка нумерации документов, обработка ошибок, блокировки под нагрузкой и хранение истории значений.

Где хранить значение счётчика

Хранилище определяет, переживёт ли счётчик параллельную работу пользователей. Вариантов три.

🔹 Константа — одно глобальное число. Подходит для файловой базы и одного счётчика на всю конфигурацию. Минус: под каждый новый счётчик нужна новая константа, а защита от одновременной записи всё равно ложится на вас.

🔹 Реквизит справочника или документа — худший вариант. Нет ни блокировки на уровне строки, ни удобного ключа. Для счётчика это тупик: дубли гарантированы под многопользовательской работой.

🔹 Независимый регистр сведений — рекомендуемый способ. Один регистр обслуживает сколько угодно счётчиков: разрезы задаются измерениями (организация, склад, год, тип документа), само значение лежит в ресурсе. Запись защищается управляемой блокировкой по ключу.

ХранилищеНесколько счётчиковЗащита от дублейИстория значений
Константанет, по одной на счётчиквручную через блокировкунет
Реквизит объектанетфактически нетнет
Регистр сведенийда, через измеренияблокировка по ключуда, если периодический

Для большинства задач создайте независимый непериодический регистр сведений, например СчётчикиНумерации:

  • измерение Организация (и при необходимости Год, Префикс, ТипОперации);
  • ресурс ПоследнийНомер типа Число, достаточной длины и с нулём знаков после запятой.

Если история значений не нужна, периодичность регистра ставьте «Непериодический» — так меньше данных и быстрее запись.

Как получить следующий номер без дублей

Ключевая часть — атомарно прочитать текущее значение, увеличить его и записать обратно, не дав другому сеансу вклиниться между чтением и записью. Для этого открывают транзакцию и ставят исключительную управляемую блокировку на ключ регистра до чтения.

Блокировку ставьте ДО чтения значения, а не после. Если прочитать значение, а потом блокировать, второй сеанс успеет прочитать то же число — смысл блокировки теряется.

Функция ПолучитьСледующийНомер(Организация) Экспорт

    Результат = 0;
    НачатьТранзакцию();
    Попытка
        // 1. Блокируем ключ регистра до чтения
        Блокировка = Новый БлокировкаДанных;
        Элемент = Блокировка.Добавить("РегистрСведений.СчётчикиНумерации");
        Элемент.Режим = РежимБлокировкиДанных.Исключительный;
        Элемент.УстановитьЗначение("Организация", Организация);
        Блокировка.Заблокировать();

        // 2. Читаем текущее значение
        МенеджерЗаписи = РегистрыСведений.СчётчикиНумерации.СоздатьМенеджерЗаписи();
        МенеджерЗаписи.Организация = Организация;
        МенеджерЗаписи.Прочитать();

        // 3. Увеличиваем. Если записи ещё не было — начинаем с единицы
        Результат = ?(МенеджерЗаписи.Выбран(), МенеджерЗаписи.ПоследнийНомер, 0) + 1;

        // 4. Записываем обратно (Прочитать при неудаче очищает поля — задаём заново)
        МенеджерЗаписи.Организация = Организация;
        МенеджерЗаписи.ПоследнийНомер = Результат;
        МенеджерЗаписи.Записать();

        ЗафиксироватьТранзакцию();
    Исключение
        ОтменитьТранзакцию();
        ВызватьИсключение;
    КонецПопытки;

    Возврат Результат;

КонецФункции

Функцию размещают в общем модуле с флагом «Сервер» и вызывают из обработчика записи документа или из команды. Благодаря блокировке по значению измерения Организация счётчики разных организаций друг друга не тормозят — ждут только те сеансы, что претендуют на один и тот же ключ.

Отдельный вопрос — откуда вызывать функцию. Если счётчик привязан к документу, номер выдают в обработчике ПередЗаписью или ОбработкаПроведения серверного модуля объекта. Из клиентского кода напрямую к регистру обращаться нельзя — выдача номера это серверная операция. Менеджер записи здесь удобнее набора записей: он работает с одной строкой по полному ключу, а не выбирает коллекцию, и читается быстрее.

💡 Если нужно собрать строковый номер с префиксом и ведущими нулями, число форматируют отдельно: СтрШаблон("%1-%2", Префикс, Формат(Результат, "ЧЦ=6; ЧВН=; ЧГ=0")). Склеивать число со строкой без Формат нельзя — получите ошибку преобразования.

Перед запуском в работу проверьте реализацию по короткому списку:

Проверка счётчика перед вводом в работу
Выполнено: 0 / 5

Нумерация документов штатными средствами

Если задача звучит как «сделать сквозную нумерацию документов», самописный счётчик чаще всего избыточен. Платформа 8.3 нумерует документы сама — достаточно настроить это в конфигураторе.

Главный приём, который часто пропускают: общий нумератор. Создайте объект «Нумератор» и назначьте его нескольким документам — тогда они будут нумероваться в единой сквозной последовательности, а не каждый по себе.

В свойствах документа или нумератора задают:

  1. Тип номера — Число или Строка. Для префиксов берут строковый.
  2. Длина номера — сколько знаков резервируется.
  3. Периодичность — В пределах года, квартала, месяца, дня или Непериодический. От неё зависит, когда нумерация обнуляется.
  4. Автонумерация — платформа сама подставит следующий номер при записи нового документа.
  5. Проверка уникальности — не даст записать документ с занятым номером.

Чтобы номер обнулялся с 1 января, поставьте периодичность В пределах года — отдельный счётчик для этого не нужен.

Собственный префикс подставляют в обработчике события документа ПриУстановкеНовогоНомера:

Процедура ПриУстановкеНовогоНомера(СтандартнаяОбработка, Префикс)
    Префикс = "ЗАК-";
КонецПроцедуры

Здесь СтандартнаяОбработка оставляют включённой — тогда платформа добавит к вашему префиксу свой порядковый номер. Этого хватает для большинства сценариев: приказы, заказы, акты с буквенным кодом подразделения.

С длиной номера есть тонкость. Для строкового типа в длину входят и символы префикса, поэтому короткое поле быстро «переполнится» на сквозной нумерации — закладывайте запас. Для числового типа при переполнении платформа начнёт выдавать номера с большим числом знаков, а проверка уникальности не даст записать документ с занятым номером, если вы правили нумерацию вручную. Если префикс зависит от реквизитов самого документа (например, от подразделения), в обработчике ПриУстановкеНовогоНомера доступен объект через ЭтотОбъект — префикс можно собрать на лету.

Самописный счётчик из прошлого раздела нужен только там, где штатной нумерации мало: отдельная сквозная нумерация внутри табличной части, номера, не привязанные к объекту, или своя логика сброса.

Обработка ошибок и повторных попыток

Блок с транзакцией уязвим к двум ситуациям: исключение внутри (нарушена логика, упала запись) и тайм-аут блокировки, когда соседний сеанс держит ключ дольше допустимого.

Исключение внутри транзакции обязательно сопровождают откатом. В коде выше это делает ветка Исключение с ОтменитьТранзакцию() и ВызватьИсключение. Без ОтменитьТранзакцию() транзакция остаётся незавершённой, и следующая попытка записи упадёт с ошибкой «В данной транзакции уже происходили ошибки».

Тайм-аут блокировки по умолчанию — 20 секунд. Если ключ занят дольше, платформа возбуждает исключение. Для коротких операций выдачи номера это редкость, но под нагрузкой разумно сделать несколько повторов:

Попытка = 0;
Пока Попытка < 3 Цикл
    Попытка = Попытка + 1;
    Попытка // ключевое слово начала обработки исключения
        Номер = ПолучитьСледующийНомер(Организация);
        Прервать;
    Исключение
        Если Попытка = 3 Тогда
            ВызватьИсключение;
        КонецЕсли;
    КонецПопытки;
КонецЦикла;

Повтор имеет смысл только для ошибок блокировки и взаимоблокировок (deadlock) — их платформа «лечит» самим фактом повторного запуска транзакции. Логические ошибки повтором не исправить, поэтому в реальном коде тип исключения стоит различать по тексту через ИнформацияОбОшибке().

Не ловите исключение «молча». Если после трёх попыток номер не выдан, пользователь должен увидеть понятное сообщение, а не записать документ с нулём вместо номера.

Производительность и блокировки под нагрузкой

Счётчик — узкое место по определению: все, кто выдаёт номер по одному ключу, выстраиваются в очередь. Задача — сократить время удержания блокировки.

🔹 Блокируйте как можно позже. Ставьте блокировку непосредственно перед чтением значения, а не в начале длинной процедуры. Чем меньше кода между Заблокировать() и ЗафиксироватьТранзакцию(), тем короче очередь.

🔹 Выдавайте номер в конце записи документа. Если получить номер в самом начале проведения, блокировка счётчика будет держаться всё время, пока формируются движения по регистрам. Лучше получить номер последним действием.

🔹 Дробите счётчики измерениями. Отдельные ключи по организациям, складам или типам документов означают независимые очереди. Сеансы разных организаций не ждут друг друга.

🔹 Используйте управляемый режим блокировок. В свойствах конфигурации режим управления блокировкой данных должен быть Управляемый. Исключительная управляемая блокировка по конкретному ключу гораздо «уже» автоматической блокировки всей таблицы.

🔹 Не делайте в транзакции лишнего. Запросы к другим таблицам, запись логов, вызовы внешних систем — всё это удлиняет удержание ключа. Вынесите их за пределы блока выдачи номера.

Взаимоблокировки (deadlock) возникают, когда два процесса захватывают ресурсы в разном порядке. Для счётчиков это редкость, если один ключ блокируется один раз, но если в одной транзакции вы трогаете несколько счётчиков — блокируйте их всегда в одинаковом порядке.

Хранение истории значений счётчика

Иногда нужно знать не только текущее значение, но и каким оно было на конкретную дату — например, чтобы понять, сколько номеров выдали за месяц, или восстановить картину при разборе дублей.

Для этого регистр сведений делают периодическим. Тогда каждая запись привязана к полю Период, и платформа сама хранит значения по датам. Текущее значение на нужную дату берут срезом последних:

Отбор = Новый Структура("Организация", Организация);
Запись = РегистрыСведений.СчётчикиНумерации.СрезПоследних(ТекущаяДата(), Отбор);

Минус периодического регистра — он тяжелее непериодического: записей больше, запись чуть медленнее. Поэтому периодичность включают осознанно, когда история действительно нужна, а не «на всякий случай».

Если же нужен не журнал значений счётчика, а история изменений самих документов (кто и когда поменял номер или реквизиты), это уже другая задача — её закрывает механизм версионирования объектов из библиотеки стандартных подсистем, а не регистр счётчика.

Частые ошибки при реализации счётчика

Проблемы со счётчиком почти всегда сводятся к нескольким типовым промахам в проектировании — не в коде конкретной строки, а в подходе.

  • 🔸 Разделяемая блокировка вместо исключительной. В режиме Разделяемый два сеанса читают значение одновременно и получают один номер. Для счётчика нужен только Исключительный.
  • 🔸 Чтение без транзакции. Блокировка живёт только внутри транзакции. Без НачатьТранзакцию() вызов Заблокировать() ничего не защитит.
  • 🔸 Хранение в реквизите или в видимой форме. Значение, доступное пользователю на редактирование, рано или поздно собьют вручную.
  • 🔸 Выдача номера до проверки документа. Если номер получен, а запись документа потом отменяется, в последовательности появляется «дыра». Для многих задач это допустимо, но закладывать логику на отсутствие пропусков нельзя.

Отдельно про пропуски в нумерации: штатная автонумерация документов тоже резервирует номер при создании, и если документ не записать, номер пропадёт. Строгая нумерация «без единого пропуска» средствами платформы не гарантируется — закладывайтесь на это заранее.

Частые вопросы

Можно ли сделать счётчик на константе?
Можно, если счётчик один и база файловая. Для многопользовательской работы константу всё равно придётся читать и писать внутри транзакции с блокировкой, а под каждый новый счётчик заводить отдельную константу. Регистр сведений с измерениями удобнее и масштабируется без правок конфигурации.
Как обнулить счётчик в начале года?
Два пути. Для нумерации документов поставьте нумератору периодичность «В пределах года» — платформа сбросит нумерацию сама. Для самописного счётчика добавьте в регистр измерение Год — при смене года ключ станет другим и отсчёт начнётся с единицы.
Чем нумератор отличается от регистра сведений?
Нумератор — штатный механизм платформы для номеров документов: настраивается в конфигураторе без кода и сам резервирует номер при записи. Регистр сведений — хранилище под произвольный счётчик, которым вы управляете из кода. Если задача сводится к номерам документов, берите нумератор.
Нужна ли блокировка в файловой базе?
В файловом варианте запись и так однопоточная, поэтому дубли менее вероятны. Но код с транзакцией и блокировкой писать всё равно стоит: ту же конфигурацию могут перенести на клиент-сервер, и тогда незащищённый счётчик начнёт выдавать дубли.
Почему в номерах появляются пропуски?
Номер резервируется при создании записи. Если документ не сохранили или запись отменилась после получения номера, значение «теряется». Это штатное поведение и для автонумерации, и для самописного счётчика. Строгая сквозная нумерация без пропусков средствами платформы не гарантируется.