В многопользовательской работе на платформе 1С:Предприятие 7.7 разработчики часто сталкиваются с крайне загадочным поведением системы: два документа, проводящиеся практически одновременно (с разницей в доли секунды), успешно проходят проверку остатков в модуле проведения, хотя по логике вещей один из них должен был заблокироваться или завершиться с ошибкой нехватки товара. В результате в базе данных регистрируются отрицательные остатки, а логика учета нарушается.
Давайте подробно разберем, почему возникает этот феномен (известный в системном программировании как состояние гонки или race condition), как устроена система блокировок в «клюшках» и какие существуют практические способы решения этой проблемы.
В стандартном, ненастроенном режиме работы 1С 7.7 на MS SQL Server параллельное проведение документов физически заблокировано на уровне платформы. Разработчики платформы заложили в нее механизм монопольной блокировки общего журнала документов:
_1sp__1SJOURN_TLockX.SELECT @i=1 FROM _1SJOURN (TABLOCKX HOLDLOCK) WHERE 0=1.TABLOCKX полностью блокирует таблицу _1SJOURN на запись для любых других сеансов пользователей до тех пор, пока текущая транзакция проведения не будет зафиксирована (Commit) или отменена (Rollback).При такой схеме параллельное проведение невозможно в принципе. Если пользователь Б запускает проведение документа, пока проводится документ пользователя А, сессия пользователя Б засыпает на уровне СУБД и ждет освобождения таблицы _1SJOURN.
Но почему тогда у автора темы документы проводятся одновременно?
В высоконагруженных информационных базах блокировка общего журнала документов становится главным препятствием для масштабируемости системы. Пользователи начинают жаловаться на постоянные ожидания блокировок при попытке провести даже мелкие документы. Чтобы решить эту проблему производительности, администраторы баз данных и разработчики прибегают к оптимизации: устанавливают сторонние патчи (например, решения от компаний Softpoint, TurboBL, ВК 1С++ или вручную комментируют вызовы блокировки общего журнала в SQL-процедурах).
Как только монопольная блокировка таблицы _1SJOURN снимается ради ускорения работы, СУБД начинает обрабатывать транзакции проведения параллельно. И тут во всей красе проявляется проблема параллельного контроля остатков.
Рассмотрим по шагам, что происходит в СУБД, когда параллельно проводятся документы А и Б, содержащие одинаковый товар, при условии, что монопольная блокировка журнала отключена:
ОбработкаПроведения() вызывается функция РассчитатьРегистрыНа(ТекущийДокумент()). СУБД выполняет чтение итогов регистров по нужной номенклатуре. При этом накладываются разделяемые блокировки (Shared Locks, S-блокировки), которые совместимы с другими S-блокировками.RA*). СУБД накладывает монопольные блокировки (Exclusive Locks, X-блокировки) на записываемые строки. Транзакция А успешно фиксируется.RA*. СУБД фиксирует транзакцию Б.В итоге оба документа успешно проведены, а реальный остаток товара на складе уходит глубоко в минус. Проверка остатков не сработала, потому что на момент чтения данных в обеих транзакциях физические движения еще не были записаны в базу данных.
---
Для предотвращения подобных коллизий при включенном параллельном проведении нам необходимо гарантировать актуальность сведений об остатках при записи. Рассмотрим основные подходы к решению этой задачи.
Это самый надежный, классический способ контроля остатков в условиях многопользовательской работы. Вместо того чтобы проверять наличие товара перед записью движений, мы делаем наоборот: сначала записываем движения документа в регистр, а затем проверяем, не ушел ли остаток в минус.
Почему это работает? Как только документ начинает записывать движения в таблицы RA*, СУБД накладывает монопольные блокировки на соответствующие записи. Если параллельно проводится второй документ с тем же товаром, СУБД заставит его транзакцию подождать на этапе записи движений. Когда первый документ завершит запись, второй документ запишет свои движения поверх. После этого при расчете остатков с учетом движений текущего документа система сразу же увидит образовавшийся минус и отменит проведение.
Разберем по шагам, как переписать модуль проведения документа на этот метод:
Процедура ОбработкаПроведения()
// 1. Формируем движения документа по регистру без предварительной проверки
РегОстатки = СоздатьОбъект("Регистр.ОстаткиТоваров");
// Заполняем движения на основании табличной части документа...
ВыбратьСтроки();
Пока ПолучитьСтроку() = 1 Цикл
РегОстатки.Товар = Товар;
РегОстатки.Склад = Склад;
РегОстатки.Количество = Количество;
РегОстатки.ДвижениеРасход();
КонецЦикла;
// Принудительно записываем движения в базу данных
РегОстатки.ЗаписатьДвиженияДокумента();
// 2. Временный расчет регистров с учетом только что записанных движений текущего документа
РегКонтроль = СоздатьОбъект("Регистр.ОстаткиТоваров");
РегКонтроль.ВременныйРасчет(1);
// Важно: рассчитываем регистры ПО текущий документ включительно!
РассчитатьРегистрыПо(ТекущийДокумент());
// 3. Проверяем остатки на наличие отрицательных значений
ВыбратьСтроки();
Пока ПолучитьСтроку() = 1 Цикл
ТекОстаток = РегКонтроль.СводныйОстаток(Товар, Склад, "Количество");
Если ТекОстаток < 0 Тогда
// Если остаток отрицательный, сообщаем об ошибке и отменяем проведение
Сообщить("Недостаточно товара " + СокрЛП(Товар.Наименование) + " на складе! Не хватает: " + (-ТекОстаток), "!");
СтатусВозврата(0); // Это отменит транзакцию проведения и откатит все записи в СУБД
Возврат;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Использование метода РассчитатьРегистрыПо() вместо РассчитатьРегистрыНа() гарантирует, что при анализе итогов будут учтены движения, которые этот документ только что совершил. Если товар закончился, транзакция откатится, и движения будут удалены физически.
Если в вашей конфигурации активно используется внешняя компонента 1С++ для работы с прямыми запросами к MS SQL Server, вы можете использовать механизм блокировок СУБД для конкретных строк данных, не блокируя при этом весь журнал документов.
Для этого в транзакции проведения перед получением остатков выполняется прямой SQL-запрос к таблице остатков регистров (например, RG*) по конкретной номенклатуре с использованием хинтов UPDLOCK и HOLDLOCK.
Рассмотрим пример логики работы прямого запроса:
// Пример SQL-запроса через 1С++ для блокировки остатков по конкретному товару
ТекстЗапроса = "
|SELECT
| Sum(Balance.SP...) as Остаток
|FROM
| $РегистрИтоги.ОстаткиТоваров as Balance WITH (UPDLOCK, HOLDLOCK)
|WHERE
| Balance.$Деменция.Товар = :ВыбТовар
| AND Balance.$Деменция.Склад = :ВыбСклад
|";
Как это работает: Хинт UPDLOCK указывает SQL-серверу накладывать специальную блокировку обновления на выбранные записи до конца транзакции. Эта блокировка разрешает другим пользователям просто читать эти записи (без хинтов), но запрещает любому другому процессу накладывать еще одну блокировку UPDLOCK или X-блокировку. Таким образом, параллельный сеанс Б при попытке выполнить аналогичный запрос по этому же товару зависнет в ожидании окончания транзакции сеанса А, предотвращая параллельное списание.
В некоторых случаях можно заблокировать объекты на уровне платформы 1С. Для этого используется метод Блокировка(). Перед проведением документа программно накладывается блокировка на элементы справочника (например, справочника «Номенклатура» или «Склады»), которые задействованы в документе.
Процедура ОбработкаПроведения()
// Пытаемся заблокировать склад перед списанием
Если Склад.Выбран() = 1 Тогда
Если Склад.Блокировка(1) = 0 Тогда
Сообщить("Склад заблокирован другим пользователем. Пожалуйста, повторите попытку позже.");
СтатусВозврата(0);
Возврат;
КонецЕсли;
КонецЕсли;
// Далее стандартная проверка и проведение...
// Не забываем снять блокировку при выходе
Склад.Блокировка(0);
КонецПроцедуры
Важное примечание: Объектные блокировки 1С 7.7 хорошо работают при интерактивном вводе документов пользователями, однако в сценариях автоматического фонового проведения через внешние обработки (например, при вызове метода Провести()) они могут отрабатывать некорректно или обходиться СУБД, если транзакция стартует вне контекста формы.
Если для вашего бизнеса стабильность данных и отсутствие отрицательных остатков важнее, чем микросекундная отзывчивость интерфейса при параллельной работе, лучшим решением будет отмена неофициальных патчей блокировки общего журнала документов.
Для этого системному администратору или ведущему разработчику необходимо вернуть исходную логику в хранимую процедуру _1sp__1SJOURN_TLockX на MS SQL Server, восстановив хинты TABLOCKX HOLDLOCK. В таком случае СУБД гарантированно выстроит все проводящиеся документы в строгую очередь, исключив любые накладки и состояния гонки.