Наш читатель столкнулся с ситуацией, когда, казалось бы, идентичный код для записи движений регистра накопления в документе «Расходная накладная» ведет себя по-разному в файловой и клиент-серверной базе данных 1С:Предприятия. Он ожидал, что после установки свойства Движения.ОстаткиНоменклатуры.БлокироватьДляИзменения = Истина; и вызова метода Движения.ОстаткиНоменклатуры.Записать(); будет наложена исключительная блокировка, которая предотвратит одновременную ручную блокировку всего регистра через объект БлокировкаДанных в другом сеансе. Однако в базе MSSQL ручная блокировка отрабатывала мгновенно, не дожидаясь снятия блокировки документом, тогда как в файловой базе все работало ожидаемым образом.
Давайте вместе разберем эту проблему и выясним, почему возникает такое расхождение в поведении блокировок и как правильно их использовать в различных вариантах работы 1С:Предприятия.
Когда мы работаем с 1С:Предприятием в файловом варианте, механизм блокировок устроен относительно просто. Платформа 1С в этом режиме склонна накладывать блокировки более "грубо", то есть на уровне целых таблиц базы данных. Это означает, что при записи движений документа в регистр или при пересчете итогов, вся таблица движений и, возможно, вся таблица итогов регистра накопления блокируются целиком.
Именно такое поведение объясняет, почему в файловой базе данных мы наблюдаем ожидаемый результат. Если в одном сеансе документ записывает движения в регистр, даже если они формально не изменились при перепроведении, платформа может заблокировать соответствующие таблицы. В результате, попытка другого сеанса наложить исключительную блокировку на *весь этот же регистр* через объект БлокировкаДанных будет вынуждена ожидать снятия первой блокировки. Этот подход обеспечивает простоту реализации, но может значительно снижать параллельность работы пользователей при интенсивной нагрузке.
В клиент-серверном варианте, особенно при работе с мощными СУБД, такими как Microsoft SQL Server, платформа 1С:Предприятие использует гораздо более тонкий и сложный механизм блокировок. Цель здесь — обеспечить максимальную параллельность работы пользователей, позволяя им одновременно работать с данными, которые не пересекаются.
Управляемые блокировки 1С: В отличие от файловой базы, где блокировки часто делегируются файловой системе, в клиент-серверном варианте 1С активно использует свой собственный менеджер управляемых блокировок. Этот менеджер работает поверх блокировок СУБД, дополняя и уточняя их. Это позволяет платформе более точно контролировать доступ к данным, учитывая специфику объектов 1С (измерения регистров, реквизиты документов).
Гранулярность блокировок: В клиент-серверном режиме блокировки применяются не на уровне всей таблицы базы данных, а на уровне отдельных записей или диапазонов записей регистров. Платформа 1С понимает структуру данных (например, измерения регистра накопления: Склад, Номенклатура) и может блокировать данные гораздо точнее – по конкретным значениям этих измерений, а не таблицу целиком. Это критически важно для производительности.
Уровень изоляции транзакций: Чтобы обеспечить высокую параллельность, уровень изоляции транзакций в СУБД (например, SQL Server) часто устанавливается на более низкий уровень, например, Read Committed. Всю логику обеспечения корректности параллельной работы (управление целостностью данных) берет на себя платформа 1С, используя свои управляемые блокировки.
БлокироватьДляИзменения и метода Записать()Свойство Движения.ОстаткиНоменклатуры.БлокироватьДляИзменения = Истина; не является командой для немедленной установки блокировки в текущий момент выполнения кода. Это скорее указание для платформы, что в процессе фактической записи набора записей регистра (при вызове метода Записать()) необходимо установить управляемую исключительную блокировку. Эта блокировка действует с момента вызова метода Записать() и до завершения текущей транзакции (то есть до успешного вызова ЗафиксироватьТранзакцию() или до отмены транзакции).
Ключевым моментом, который объясняет наблюдаемое расхождение, является гранулярность и условия срабатывания блокировок в клиент-серверном режиме. Когда мы устанавливаем БлокироватьДляИзменения = Истина, платформа накладывает блокировку на все комбинации измерений, присутствующие в записываемом наборе записей, игнорируя при этом "разделитель итогов" (если он настроен для данного регистра). Это обеспечивает более широкую, но все еще гранулярную блокировку внутри набора записей.
Однако, здесь есть один очень важный нюанс, который часто упускается из виду и стал причиной заблуждения автора темы: в клиент-серверном варианте 1С при перепроведении документа, если фактические данные движений не изменяются, платформа может не накладывать блокировки на эти неизмененные записи. Блокировка устанавливается только на те записи, которые действительно были изменены или добавлены в ходе операции записи.
Рассмотрим пример кода автора из Сообщения 1:
// Формирование набора записей, запись движений в регистр
Пока Выборка.Следующий Цикл
Движение = Движения.ОстаткиНоменклатуры.ДобавитьРасход;
Движение.Период = Дата;
Движение.Склад = Склад;
Движение.Номенклатура = Выборка.Номенклатура;
Движение.Количество = Выборка.Количество;
КонецЦикла;
Сообщить("Время записи проводок документа" + ТекущаяДатаСеанса);
Движения.ОстаткиНоменклатуры.БлокироватьДляИзменения = Истина;
Движения.ОстаткиНоменклатуры.Записать; // Здесь уже должна работать блокировка на регистр Остатки номенклатуры.
Если этот документ перепроводится, но при этом состав и количество номенклатуры по складам не меняются, то фактических изменений в регистре не происходит. В этом случае, платформа в клиент-серверном режиме может счесть, что блокировать "старые" неизменённые записи не требуется, так как к ним не производится никаких изменений. Именно это и объясняет, почему в другом сеансе ручная блокировка не ожидала: она пыталась заблокировать весь регистр (в том числе те записи, которые не были затронуты текущей транзакцией записи документа).
БлокировкаДанныхТеперь давайте разберем код, который использовал автор для ручной блокировки в другом сеансе:
Попытка
Сообщить("Установили блокировку " + ТекущаяДатаСеанса);
НачатьТранзакцию(РежимУправленияБлокировкойДанных.Управляемый);
Блокировка = новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ОстаткиНоменклатуры");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать;
ЗафиксироватьТранзакцию;
Сообщить("Зафиксировали блокировку " + ТекущаяДатаСеанса);
Исключение
Сообщить("Ошибка блокировки " + ТекущаяДатаСеанса);
ОтменитьТранзакцию;
КонецПопытки;
В этом коде мы создаем объект БлокировкаДанных и добавляем элемент блокировки для всего регистра накопления ОстаткиНоменклатуры, не указывая конкретные значения измерений (например, Номенклатура или Склад). В клиент-серверном варианте это означает попытку наложить очень широкую блокировку, возможно, на уровне таблицы или ее значительной части, но без привязки к конкретным записям.
Поскольку платформа при записи движений через Движения.Записать() устанавливает гранулярные блокировки на конкретные измененные записи, а ручная блокировка пытается заблокировать *весь регистр без указания конкретных значений*, эти два типа блокировок могут просто не конфликтовать. Ручная блокировка пытается занять "весь этаж", в то время как платформенная блокировка занимает "конкретные кабинеты" на этом этаже, и только если кто-то пытается занять тот же "кабинет", возникнет конфликт.
Для того чтобы ручная блокировка конфликтовала с платформенными гранулярными блокировками, она должна быть такой же гранулярной. Мы должны точно указать, какие именно данные следует блокировать, используя метод ЭлементБлокировки.УстановитьЗначение() для измерений регистра. Например, если мы хотим заблокировать остатки по конкретной номенклатуре:
Попытка
Сообщить("Установили блокировку " + ТекущаяДатаСеанса);
НачатьТранзакцию(РежимУправленияБлокировкойДанных.Управляемый);
Блокировка = новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ОстаткиНоменклатуры");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
// Вот здесь мы явно указываем, что именно блокируем
ЭлементБлокировки.УстановитьЗначение("Номенклатура", СсылкаНаНоменклатуру);
ЭлементБлокировки.УстановитьЗначение("Склад", СсылкаНаСклад);
Блокировка.Заблокировать;
ЗафиксироватьТранзакцию;
Сообщить("Зафиксировали блокировку " + ТекущаяДатаСеанса);
Исключение
Сообщить("Ошибка блокировки " + ТекущаяДатаСеанса);
ОтменитьТранзакцию;
КонецПопытки;
Если бы платформа в первом сеансе заблокировала остатки именно по этой Номенклатуре и Складу (потому что по ним были реальные изменения при записи документа), то тогда наша ручная блокировка в другом сеансе, настроенная таким образом, действительно стала бы ожидать.
Различия в механизмах блокировок: Ключевая причина наблюдаемого поведения кроется в принципиальном различии механизмов блокировок между файловой и клиент-серверной базами данных 1С. В файловой базе блокировки накладываются на уровне таблиц, что является "грубым" методом и может блокировать весь регистр. В клиент-серверной базе используются управляемые блокировки на уровне записей, что обеспечивает высокую гранулярность.
Блокировка только измененных данных: В клиент-серверном режиме платформа стремится к максимальной параллельности. При вызове Движения.Записать() блокировки накладываются только на фактически измененные или добавленные записи. Если документ перепроводится без реальных изменений в движениях регистра, платформа может не накладывать блокировки на эти неизменённые данные. Это стало ключевым моментом в разрешении недопонимания автора (Сообщение 10).
Гранулярность ручных блокировок: При использовании объекта БлокировкаДанных в клиент-серверном варианте, крайне важно указывать конкретные значения измерений (с помощью ЭлементБлокировки.УстановитьЗначение()), чтобы блокировать именно те данные, которые нас интересуют. Попытка заблокировать "весь регистр" без указания измерений может не конфликтовать с более гранулярными блокировками, установленными платформой.
Назначение БлокироватьДляИзменения = Истина: Это свойство дает платформе указание установить исключительную блокировку на комбинации измерений, входящих в *записываемый* набор записей, игнорируя при этом разделитель итогов. Блокировка устанавливается в момент вызова Записать() и действует до окончания транзакции.
Для программистов 1С, работающих с клиент-серверными базами данных, критически важно понимать эти нюансы. Неправильное использование или непонимание механизмов блокировок может привести к взаимоблокировкам, снижению производительности или, как в нашем случае, к ложному ощущению отсутствия блокировок там, где они на самом деле работают, но на другом уровне гранулярности. Всегда стремитесь к максимально возможной гранулярности блокировок, чтобы обеспечить эффективную и параллельную работу системы.