Взаимоблокировки при записи в регистры 1С на PostgreSQL: что показал стенд на 20 потоках
База данных - HighLoad оптимизация
Файлы
ВНИМАНИЕ: Файлы из Базы знаний - это исходный код разработки. Это примеры решения задач, шаблоны, заготовки, "строительные материалы" для учетной системы. Файлы ориентированы на специалистов 1С, которые могут разобраться в коде и оптимизировать программу для запуска в базе данных. Гарантии работоспособности нет. Возврата нет. Технической поддержки нет.
Подписка PRO — скачивайте любые файлы со скидкой до 85% из Базы знаний
Оформите подписку на компанию для решения рабочих задач
Оформить подписку и скачать решение со скидкойВы можете заказать платную доработку или адаптацию этой разработки под вашу конфигурацию на «Бирже заказов».
- 0% комиссии — оплата напрямую исполнителю;
- Исполнители любого масштаба — от отдельных специалистов до команд под проект;
- Прямой обмен контактами между заказчиком и исполнителем;
- Безопасная сделка — при необходимости;
- Рейтинги, кейсы и прозрачная система откликов.
Где на самом деле возникают взаимоблокировки при параллельной записи в регистр 1С на PostgreSQL и что из привычных советов на них влияет. Способы записи движений проверены на 20 потоках, механика СУБД - прямыми сеансами psql, журнал - на трёх порогах.
- Взаимоблокировку даёт захват блокировок по частям: по строке в цикле или разделяемая, потом исключительная.
- При Набор.Записать() платформа сама ставит исключительную блокировку на все строки набора одним вызовом.
- Сортировка нужна только построчному захвату и превращает ошибки в очередь: медиана 958 → 2575 мс.
- В PostgreSQL ждут ShareLock на transactionid; RowExclusiveLock в цикле не участвует, корень показывает pg_blocking_pids().
- В фильтре logcfg.xml duration в сотнях микросекунд, в записях журнала - в микросекундах.
Брать управляемую блокировку на все строки документа одним вызовом и сразу в нужном режиме; сортировка строк от взаимоблокировок при таком захвате не нужна.
Для кого: разработчики и администраторы 1С, у которых при параллельном проведении документов появляется «Конфликт блокировок при выполнении транзакции».
- Стенд
- Где лежат движения регистра в PostgreSQL
- Две разные взаимоблокировки
- Взаимоблокировка строк в PostgreSQL 16
- Кто кого держит: дерево ожиданий
- Что делает платформа при записи набора
- Способы записи движений: замер
- Что видит пользователь
- Технологический журнал: что ловить и в каких единицах
- Как записывать движения
- Предельный случай: одна блокировка на всё проведение
- Практические правила
- Что не проверялось
- Итог
- Источники
- Файл к статье
Когда два оператора одновременно проводят документы с одними и теми же товарами, один из них иногда получает «Конфликт блокировок при выполнении транзакции». Обычный совет на этот случай - сортировать строки перед записью, чтобы все транзакции брали ресурсы в одном порядке. Я проверила этот совет на стенде: 1С:Предприятие 8.3.27 на PostgreSQL, учебная конфигурация «Нано-УТ», 20 параллельных сеансов и по 2000 документов на каждый способ записи движений. Сортировка помогла там, где блокировки берутся по строке в цикле, и не повлияла ни на что остальное. Ниже - где именно возникает взаимоблокировка, что при записи набора делает сама платформа, как найти виновника в PostgreSQL и в технологическом журнале и какие настройки журнала на самом деле ничего не ловят.
Всё запускалось на одном компьютере 21 октября 2026 года; журналы, снимки и стенд лежат в архиве к статье.
Стенд
| Условие | Значение |
|---|---|
| Платформа | 1С:Предприятие 8.3.27.2130 (x86), отдельный экземпляр кластера (ragent на портах 1740/1741), серверная база |
| СУБД для базы 1С | Postgres Pro 1C 17.10 в контейнере Docker. Сборки PostgreSQL 16 для 1С на стенде нет |
| СУБД для опыта без 1С | PostgreSQL 16.15 (официальный образ postgres:16), deadlock_timeout = 1s, log_lock_waits = on |
| Конфигурация | «Нано-УТ 2.0» - наша учебная торговая конфигурация: справочники «Номенклатура», «Склады», документ «ПродажаТовара», регистр остатков «ТоварыНаСкладах» (измерения Склад и Номенклатура, ресурсы Количество и Стоимость), управляемый режим блокировок. Для опыта добавлены общий модуль ДедлокСтенд и обработка СтендВзаимоблокировок |
| Нагрузка | 20 фоновых заданий кластера (отдельные сеансы и соединения с СУБД), в каждом 100 документов по 5 строк; строки в документе в случайном порядке, как их набирает оператор; внутри транзакции пауза 20 мс - расчёт, пока блокировки удерживаются |
| Компьютер | Intel Core i5-2400 3,1 ГГц (4 ядра, 2011 год), 7,9 ГБ ОЗУ, SSD Samsung 850 EVO (SATA), Windows 11 Pro 10.0.22631; Docker Desktop 29.8.0, ВМ на 4 ядра. Компьютер рабочий: во время замеров были открыты браузер и приложение Claude |
Миллисекунды в таблицах ниже получены на этом старом компьютере и на сервере будут другими. Доли ошибок и то, какие блокировки берутся, от железа не зависят.
Где лежат движения регистра в PostgreSQL
В базе стенда регистр «ТоварыНаСкладах» лежит в двух таблицах; номера в именах платформа выдаёт сама, у вас они будут свои:
| Таблица | Что в ней | Колонки (по information_schema.columns) |
|---|---|---|
_accumrg246 |
движения | _period, _recordertref, _recorderrref, _lineno, _active, _recordkind, _fld247rref (склад), _fld248rref (номенклатура), _fld249 и _fld250 (количество и стоимость) |
_accumrgt251 |
итоги по периодам | _period, _fld247rref, _fld248rref, _fld249, _fld250 |
Ссылочное измерение в PostgreSQL - колонка _fldNNNrref типа bytea. Регистратором этого регистра бывают документы нескольких видов, поэтому рядом с _recorderrref есть _recordertref - номер типа регистратора. Настройки итогов хранятся в отдельных таблицах _accumrgoptNNN (_regid, _actualperiod, _usetotals, _usesplitter); итогов в них нет, а номер в имени с номером регистра не совпадает.
Эти же номера встречаются в технологическом журнале: пространство управляемых блокировок AccumRg246.DIMS - измерения регистра _accumrg246, поля Fld247 и Fld248 - склад и номенклатура.
Две разные взаимоблокировки
В базе 1С на PostgreSQL взаимоблокировку может обнаружить любая из двух сторон.
СУБД. Если две транзакции обновляют одни и те же строки в разном порядке, PostgreSQL через deadlock_timeout находит цикл и прерывает одну из них с ERROR: deadlock detected.
Менеджер управляемых блокировок 1С. В управляемом режиме платформа сначала ставит собственные блокировки на пространства данных (регистр, измерения, значения) и только потом обращается к СУБД. Если цикл образуется на этих блокировках, менеджер 1С находит его сам. Пользователь видит «Неустранимый конфликт блокировок», а технологический журнал пишет событие TDEADLOCK.
На стенде за 12 000 документов все 3876 взаимоблокировок были второго вида; deadlock detected от PostgreSQL не появился ни разу. Первый вид я воспроизвела отдельно, прямыми запросами к PostgreSQL 16, чтобы показать механику СУБД.
Взаимоблокировка строк в PostgreSQL 16
Опыт без 1С: таблица итогов из трёх строк, два сеанса psql. S1 обновляет «Товар А», потом «Товар Б». S2 обновляет «Товар Б», потом «Товар А». Третье окно в момент ожидания снимает pg_locks и pg_blocking_pids().


Что видно на снимках:
RowExclusiveLockна таблицуtotalsвыдана обоим сеансам одновременно (granted = t). Это блокировка уровня таблицы, которую берёт любойUPDATE,INSERTилиDELETE. Сама с собой она не конфликтует, поэтому к взаимоблокировке отношения не имеет.- Ждёт сеанс другого: S1 запрашивает
ShareLockнаtransactionidтранзакции S2, то есть ждёт её завершения. Когда к одной строке выстраивается очередь, следующий в очереди ещё держитExclusiveLockнаtuple. - Текст ошибки СУБД называет обе стороны: «Process 280 waits for ShareLock on transaction 751; blocked by process 281. Process 281 waits for ShareLock on transaction 752; blocked by process 280».
- Цикл находит сам ждущий процесс, отдельного фонового процесса для этого нет. В журнале сервера: «process 117 detected deadlock while waiting for ShareLock on transaction 745 after 1000.277 ms». В документации
deadlock_timeoutописан так же: столько сеанс ждёт, прежде чем проверить, не попал ли он в цикл; по умолчанию 1 секунда. - Когда оба сеанса берут строки в одном порядке, второй ждёт первого на первой же строке и после его фиксации проходит без ошибки.
Кто кого держит: дерево ожиданий
Для поиска корня очереди удобна функция pg_blocking_pids(pid): она возвращает сеансы, которые мешают данному получить блокировку, и сама учитывает очередь к строке. Запрос из архива строит по ней дерево: корень - сеанс, который держит блокировку и сам никого не ждёт.
На очереди из трёх сеансов запрос показал корень - S2 в состоянии idle in transaction, под ним S3 (Lock:transactionid) и S1 (Lock:tuple). Полная версия в архиве (lock_tree.sql) дополнительно выводит сеансы неразорванного цикла с пометкой «цикл»: при deadlock_timeout = 10s в снимок попали оба участника. Корень цикла найти нельзя, у цикла его нет.
Распространённый вариант такого запроса соединяет pg_locks ожидающего и держащего по всем полям и потом рекурсивно наращивает цепочку. Я запустила его на той же очереди. Корневого сеанса в выводе нет. Одна и та же пара ожидания вышла дважды, на глубине 1 и 2: рекурсия у этого запроса стартует сразу со всех пар ожидания.
Что делает платформа при записи набора
Первый вариант серии - записать движения без всякой явной блокировки, только Набор.Записать(). Строки 5 товаров в каждом из 2000 документов шли в случайном порядке, 20 потоков одновременно. Ошибок - ноль.
Причина видна в технологическом журнале. На строке Набор.Записать() платформа сама ставит исключительную управляемую блокировку на все строки набора сразу:
TLOCK ... Regions=AccumRg246.DIMS,Locks='AccumRg246.DIMS Exclusive Period=[T"20261001000000":+] Fld247=55:...e8 Fld248=54:...ea, Period=[T"20261001000000":+] Fld247=55:...e8 Fld248=54:...ec, ... (все 5 строк)', WaitConnections=88, Context='ОбщийМодуль.ДедлокСтенд.Модуль : 119 : НовыйНабор(Ссылка, Т).Записать(Истина);'
Блокировка захватывает все комбинации измерений набора одним вызовом, поэтому порядок строк в наборе для неё не важен. В 1C:Developer Network об этом сказано так же: при записи набора записей платформа сама устанавливает управляемую блокировку.
Способы записи движений: замер
Каждый поток создавал непроведённый документ «ПродажаТовара» и в своей транзакции писал по нему расход в регистр «ТоварыНаСкладах», в обход учётного движка «Нано-УТ». Отличался только способ блокировки.
| Способ записи | Ошибок из 2000 | Медиана, мс | p95, мс |
|---|---|---|---|
Только Набор.Записать() |
0 | 697 | 1050 |
БлокировкаДанных на каждую строку по очереди, в порядке документа |
1982 (99,1 %) | 958 | 1560 |
| То же после сортировки строк по номенклатуре | 0 | 2575 | 2705 |
Одна БлокировкаДанных на все строки (ИсточникДанных), без сортировки |
0 | 865 | 910 |
| Одна блокировка на отсортированные строки (процедура с сортировкой, которую часто приводят как образец) | 0 | 753 | 967 |
| Разделяемая блокировка всех строк, потом исключительная; строки отсортированы | 1894 (94,7 %) | 252 | 371 |
Текст каждой ошибки - «Конфликт блокировок при выполнении транзакции: Неустранимый конфликт блокировок»; событий TDEADLOCK в журнале столько же, сколько ошибок (3876). После каждого прогона остаток каждого товара равен начальному 1 000 000 минус число успешных документов: откаты не оставили частичных движений.
Что из этого следует:
- Взаимоблокировка появляется, когда транзакция захватывает ресурсы по частям. При построчной блокировке в порядке документа одна транзакция может держать, например, «Товар 03» и ждать «Товар 01», а другая - наоборот. Из 2000 документов прошли 18.
- Сортировка помогает только при таком захвате по частям. С отсортированными строками ошибок не стало, но медиана выросла с 958 до 2575 мс: транзакции встали в очередь на первом общем товаре и держат его всё время своей работы.
- Одна блокировка на весь набор работает без сортировки. Ни одной ошибки при случайном порядке строк, как и у самой платформы при
Записать(). Сортировка перед такой блокировкой на результат не влияет. - Сортировка не спасает от повышения блокировки. Две транзакции берут разделяемую блокировку на одни и те же товары (например, читают остатки для контроля), потом обе просят исключительную - и ждут друг друга при любом порядке строк. 94,7 % ошибок. Документация PostgreSQL и 1C:Developer Network советуют на этот случай одно и то же: первой брать блокировку того режима, который понадобится для изменения.
Что видит пользователь
Обработка стенда открывает второй сеанс фоновым заданием: он берёт «Товар 02», затем «Товар 01», а текущий сеанс - «Товар 01», затем «Товар 02».

Если исключение сервера не перехвачено в форме, клиент показывает только общее окно «К сожалению, возникла непредвиденная ошибка», а текст про конфликт блокировок спрятан за ссылкой «Сформировать отчет об ошибке».

Кнопка «Товары в одном порядке» проводит тот же опыт с одинаковым порядком: второй сеанс ждёт, ошибки нет. Ждать он может долго: таймаут ожидания управляемой блокировки в базе по умолчанию 20 секунд (ПолучитьВремяОжиданияБлокировкиДанных() вернула 20), после чего ожидание прерывается с «Превышено максимальное время ожидания предоставления блокировки».

Технологический журнал: что ловить и в каких единицах
Для разбора взаимоблокировок управляемого режима нужны три события:
TDEADLOCK- менеджер блокировок 1С обнаружил цикл;TTIMEOUT- ожидание прервано по таймауту;TLOCK- установка управляемой блокировки; длинные ожидания показывают, кто кого держит (WaitConnections).
Порог 30000 здесь означает 3 секунды. В фильтре logcfg.xml длительность задаётся в сотнях микросекунд, а в самих записях журнала пишется в микросекундах. Я проверила это на стенде: держала блокировку 4,5 секунды. Журнал с порогом 30000 записал TLOCK с длительностью 3 921 981 мкс. Журнал с порогом 3000000 за весь день не записал ни одного TLOCK. 3000000 - это 300 секунд, больше таймаута 20 с, так что такое ожидание не наступит никогда. События TDEADLOCK этот фильтр по длительности не затрагивает, они записались все 3908.

У TDEADLOCK два содержательных свойства:
DeadlockConnectionIntersections- пары соединений цикла: кто кого ждёт, в каком пространстве (AccumRg246.DIMS), в каком режиме и на каких значениях измерений (Fld247- склад,Fld248- номенклатура; ссылка записана как номер типа и её значение);Context- стек встроенного языка доЗаблокировать().
Текста SQL в этом событии нет: до СУБД дело ещё не дошло.
Как записывать движения
Код варианта «одна блокировка на все строки» из модуля стенда; в серии с ним не упал ни один документ. Транзакцию открывает вызывающий: в документе это обработка проведения.
Две детали, которые я нашла, проверяя на стенде процедуру с сортировкой:
- Процедура в общем модуле с директивой
&НаСервереперед ней проходит/CheckModulesбез ошибок, а при вызове платформа отвечает «Метод объекта не обнаружен». В общем модуле директивы компиляции перед процедурой не нужны: где он компилируется, задают его свойства. - Вне транзакции
Блокировка.Заблокировать()выдаёт «Использование блокировки допустимо только внутри транзакции в режиме управляемых блокировок!».
Предельный случай: одна блокировка на всё проведение
В «Нано-УТ 2.0» учётный движок при каждом проведении берёт одну исключительную блокировку на служебный ключ учёта (РегистрСведений.БлокировкаУчета) и пересчитывает хронологию. Я провела им 100 документов в 20 потоков. Взаимоблокировок не было ни одной: два проведения просто не могут идти одновременно. Цена - очередь: медиана 14,2 секунды на документ, и 14 документов из 100 не дождались блокировки за таймаут базы (20 секунд, см. выше; «Превышено максимальное время ожидания предоставления блокировки»).
Для учебной базы это осознанный выбор, для рабочей базы с десятками операторов - нет. Между «каждая строка своей блокировкой» и «одна блокировка на всё» стоит рабочий вариант из таблицы выше: одна блокировка на строки своего документа.
Практические правила
- Брать управляемую блокировку на все строки документа одним вызовом
Заблокировать()и до чтения остатков. Блокировка по строке в цикле - главный источник взаимоблокировок в замере. - Сразу брать исключительный режим, если данные будут изменены. Разделяемая блокировка с последующей исключительной даёт взаимоблокировки при любом порядке строк.
- Сортировать строки имеет смысл, только если захват по частям неизбежен (например, блокировки в разных процедурах одной транзакции). Тогда порядок должен быть одинаковым во всех документах, которые пишут в этот регистр.
- Не делать долгих расчётов под блокировкой. В нашем замере при построчной блокировке отсортированных строк пауза в 20 мс под блокировкой подняла медиану документа из 5 строк с 958 мс до 2,6 секунды: транзакции стояли в очереди.
- Ловить
TDEADLOCK,TTIMEOUTи длинныеTLOCKв технологическом журнале, порог для TLOCK задавать в сотнях микросекунд. - Искать корень очереди в PostgreSQL через
pg_blocking_pids(); если в базе 1С виденERROR: deadlock detected, транзакции захватывали строки СУБД без управляемых блокировок (автоматический режим или прямые запросы).
Что не проверялось
Автоматический режим блокировок; база 1С на PostgreSQL 16 сборки для 1С (у нас Postgres Pro 1C 17.10, PostgreSQL 16.15 - только для опыта без 1С); сервер 1С на Linux; типовые конфигурации (УТ 11, ERP); регистры с разделением итогов; документы длиннее 5 строк и нагрузка больше 20 потоков.
Итог
- В управляемом режиме взаимоблокировки на стенде находил менеджер блокировок 1С (
TDEADLOCK, «Неустранимый конфликт блокировок»); PostgreSQL за 12 000 документов не выдал ни одногоdeadlock detected. - Причина взаимоблокировок - захват по частям: построчная блокировка в порядке документа (99,1 % ошибок) и повышение разделяемой блокировки до исключительной (94,7 %).
Набор.Записать()и однаБлокировкаДанныхна весь набор блокируют все строки сразу и не дают взаимоблокировок при любом порядке строк; сортировка нужна только построчному захвату и превращает ошибки в очередь.- В PostgreSQL ждут блокировку транзакции (
ShareLockнаtransactionid);RowExclusiveLockв цикле не участвует. Корень очереди показываетpg_blocking_pids(). - Порог TLOCK в
logcfg.xmlзадаётся в сотнях микросекунд:30000- 3 секунды,3000000- 300 секунд.
Источники
- PostgreSQL 16, раздел 13.3 «Explicit Locking» (табличные и строковые блокировки, взаимоблокировки, единый порядок захвата): https://www.postgresql.org/docs/16/explicit-locking.html
- PostgreSQL 16, параметр
deadlock_timeout: https://www.postgresql.org/docs/16/runtime-config-locks.html - PostgreSQL 16, функция
pg_blocking_pids: https://www.postgresql.org/docs/16/functions-info.html - 1C:Developer Network, «Transaction Lock Management» (управляемый режим, блокировка при записи набора записей): https://1c-dn.com/1c_enterprise/transaction_lock_management/
- 1C:Developer Network, «Avoiding deadlocks» (единый порядок доступа, блокировка на изменение при чтении): https://1c-dn.com/library/avoiding_deadlocks/
- Руководство администратора 1С:Предприятия 8.3,
logcfg.xml: https://kb.1ci.com/1C_Enterprise_Platform/Guides/Administrator_Guides/1C_Enterprise_8.3.23_Administrator_Guide/Appendix_3._Description_and_location_of_internal_files/3.23._logcfg.xml/3.23.1._General_description
Файл к статье
Архив pg-deadlock-stand-companion.zip (стенд от 21.10.2026):
pg/pg_deadlock_demo.pyиpg/lock_tree.sql- опыт на PostgreSQL без 1С, нужен только Docker и Python 3;1c/config- XML-выгрузка конфигурации стенда («Нано-УТ 2.0» с модулемДедлокСтенди обработкойСтендВзаимоблокировок);- скрипты загрузки (
designer.py), серии (run-series.ps1), сводки (analyze.py) и снимков клиента; logcfg.xmlс порогом TLOCK 3 секунды;evidence- журналы и таблицы этого прогона, образцы событий TDEADLOCK и TLOCK, рисунки статьи.
В README.md - порядок запуска: свой экземпляр кластера от обычного пользователя, база на PostgreSQL для 1С, загрузка выгрузки, серия из 32-битного PowerShell через V83.COMConnector. Архив проверен из чистой распаковки: опыт на PostgreSQL повторён, выгрузка загружена в новую серверную базу, короткая серия дала тот же результат.
Вступайте в нашу телеграмм-группу Инфостарт