Казалось, о работе сервера 1С и механизма блокировок за годы разработки уже знаешь всё, но 1С не перестаёт удивлять. Механизм управляемых блокировок создан 1С для того, чтобы по сути полноценно заменить механизм транзакционных блокировок на уровне сервера СУБД, отчасти из за политики "1С работает с любыми СУБД". Но на самом деле всё получилось не совсем так - далее проведём "лабораторную работу", и сделаем из неё выводы - в лучших традициях школьной физики :).
Итак начнём - простой кейс:
Конфигурация 2 объекта РС и Документ.
Регистр сведений независимый, код следующий:
Если Блокировать Тогда
Блокировка = Новый БлокировкаДанных();
Элемент = Блокировка.Добавить("РегистрСведений.РегистрСведений1");
Элемент.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
КонецЕсли;
Если ЧитатьЗапросом Тогда
Запрос = Новый запрос();
Запрос.Текст = "ВЫБРАТЬ
| РегистрСведений1.Рес КАК Рес
|ИЗ
| РегистрСведений.РегистрСведений1 КАК РегистрСведений1";
ТЗ = Запрос.Выполнить().Выгрузить();
КонецЕсли;
Если ЧитатьНабор Тогда
Набор = РегистрыСведений.РегистрСведений1.СоздатьНаборЗаписей();
Набор.Прочитать();
КонецЕсли;
Если ЗаписатьНабор Тогда
Запись = набор.Добавить();
Запись.Изм = Строка(Новый УникальныйИдентификатор());
Запись.Рес = 3;
Набор.Записать();
КонецЕсли;
Всё предельно просто.
Теперь Запускаем два сеанса.
Сеанс 1: Проводим документ с галкой "Блокировать". Ставим точку останова на строке:
"Если ЧитатьЗапросом Тогда".
Сеанс 2: Проводим документ с галкой "Читать запросом".
Внимание вопрос: "Проведётся ли документ во втором сеансе или нет?"
Нам бы очень хотелось чтобы ответ на данный вопрос был "нет".
Тем не менее, документ проводится - без особых проблем.
Повторяем эксперимент - только во втором сеансе ставим галку "чтение набора"
Тот же вопрос "Проведётся?"
Очевидный ответ - "Конечно", ведь по сути что там что там чтение всего регистра.
Тем не менее наблюдаем примерно следующую историю:
Таким образом, объектное чтение и чтение запросом работает по-разному. На мой взгляд, это в корне неправильно. При этом объектное чтение, очевидно, учитывает управляемые блокировки, а чтение запросом - ни коим образом.
Спойлер - эти тесты проводим пока в режиме "клиент-сервер".
Из эксперимента выше сделаем несколько первых выводов:
Вывод 1: 1С полностью игнорирует все управляемые блокировки при чтении в транзакции запросом.
Вывод 2: Объектное чтение и чтение запросом работает по разному
Тут можно сказать: "здравствуй фантомное чтение, неповторяющееся чтение". От "Грязного чтения" нас убережет MS SQL: в уровне изоляции READ COMMITED SNAPSHOT "грязное чтение" невозможно. Хотя я тоже сомневаюсь, потому как объектная модель 1С не полностью отражается в СУБД возможно могут быть нюансы, но примеров подобрать пока не удаётся.
Для того, чтобы понять на каком этапе своего развития в платформе 1С потеряли требование "согласованности данных" обратимся к истории.
Попробуем выполнить следующий код в разных версиях платформы:
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| СУММА(РегистрСведений1.Рес) КАК Рес
|ИЗ
| РегистрСведений.РегистрСведений1 КАК РегистрСведений1
|
|ДЛЯ ИЗМЕНЕНИЯ";
Выборка = запрос.Выполнить().Выбрать();
Выборка.Следующий();
Количество = Выборка.Рес;
Если Количество > 0 Тогда
Запись = РегистрыСведений.РегистрСведений1.СоздатьМенеджерЗаписи();
Запись.Изм = Строка(Новый УникальныйИдентификатор());
Запись.Рес = -1;
Запись.Записать();
КонецЕсли;
Я специально избегаю регистров накопления в примерах, потому как в РН как минимум две таблицы на уровне СУБД, а хочется продемонстрировать на одной.
Этот код выполняется в транзакции - в обработке проведения документа в моём случае. Первую транзакцию нужно конечно остановить и попытаться провести вторую. По условию мы не должны уйти в минус.
1С 8.1 и ранее (ну или просто Автоматический режим блокировки)
второй документ "висит на блокировке". После прохождения точки останова второй документ проводится, запись в РС не создаётся.
Код отрабатывает правильно как в случае остановки "после записи" так и в случае "после чтения".
Секрет конечно в инструкции "для изменения", которая кроме всего прочего превращает S блокировку в U.
На уровне СУБД уровень изоляции MS SQL - SERIALIZABLE. MS SQL блокирует всё что надо и не надо. Чтобы получить несогласованные данные надо очень постараться.
Но с параллельной работой в таком варианте конечно будут трудности, всем известно какие.
1С 8.2 и первые версии 8.3 (режим управляемых блокировок)
Появились "Управляемые блокировки". Самое главное что происходит с MS SQL при установке "управляемой блокировки" - уровень изоляции становится "READ COMMITED".
Чтобы включить его в последних редакциях 8.3 нужно выполнить:
ALTER DATABASE databasename
SET ALLOW_SNAPSHOT_ISOLATION OFF
GO
ALTER DATABASE databasename
SET READ_COMMITTED_SNAPSHOT OFF
GO
Здесь уже немного похуже. Если точку останова в коде поставить после записи - всё отработает как и в предыдущем примере - транзакция установит X блокировку и всё будет OK. А вот если точку останова поставить после чтения - получим совместимые S блокировки - одна и та же информация будет считана двумя транзакциями.
Конечно данный уровень изоляции уже не охраняет нас от ошибок "фантомного" и "неповторяемого" чтения.
В приведённом примере, тем не менее, в большинстве случаев всё будет работать верно, потому что всё-таки поведение системы в транзакции более-менее прогнозируемо - если данные считаны, то на них устанавливается как минимум S блокировка.
Конечно нужно добавить в этот код начало вроде:
Блокировка = Новый БлокировкаДанных();
Элемент = Блокировка.Добавить("РегистрСведений.РегистрСведений1");
Элемент.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
и нам уже ничего не страшно.
Зачем я тогда делаю на этом акцент?
В данном примере вы читаете остатки (по сути) - их конечно нужно блокировать.
Но если у вас в транзакции происходит чтение элемента справочника, который в данный момент может изменять другая транзакция?
Вот тут как раз S блокировка очень сильно пригодилась бы. Но об этом дальше.
Современные версии 1С 8.3
Возвращаем обратно режим версионника:
ALTER DATABASE databasename
SET ALLOW_SNAPSHOT_ISOLATION ON
GO
ALTER DATABASE databasename
SET READ_COMMITTED_SNAPSHOT ON
GO
Выполняем код - получаем "-1" как в случае установки "точки останова" при чтении, так и в случае установки "точки останова" при записи.
Теперь можно сделать ещё один вывод:
Вывод 3: чем современнее версия 1С, тем больше возможности для параллельной работы и тем меньше шансов обеспечить согласованность данных.
А теперь "Гвоздь программы": файловая база - код приведенный в блоке кода 1 выполняем в ней. Единственное изменение - код чтения данных из регистра придётся перенести в обработку, т.к. в файловой базе два одинаковых документа параллельно провести не получится.
В обработке будет код:
НачатьТранзакцию();
Запрос = Новый запрос();
Запрос.Текст = "ВЫБРАТЬ
| РегистрСведений1.Рес КАК Рес
|ИЗ
| РегистрСведений.РегистрСведений1 КАК РегистрСведений1";
ТЗ = Запрос.Выполнить().Выгрузить();
ЗафиксироватьТранзакцию();
В документе ставим галку "блокировать" и точку останова, выполняем обработку - уверенно висит на блокировке.
Убираем галку "блокировать" в документе (но не точку останова) конечно без проблем читает регистр.
Итак, в завершение "чудес" управляемых блокировок получаем их разную работу в файловой и клиент-серверной версии.
Вывод 4: Управляемые блокировки в файловой и клиент-серверной версиях работают по-разному. В файловой - "нормально".
Теперь о косяках в типовых. Для примера возьму УТ 11.
Перед записью в набор движения естественно читаются, происходит это в функции "ТекстЗапросаТаблицаТоварыНаСкладах(Запрос, ТекстыЗапроса, Регистры)" - для Товаров на складах.
Так вот, "косяк" потенциально будет во всех строчках этого запроса где "точка" фигурирует дважды:
"КОГДА ЕСТЬNULL(ТаблицаТовары.Назначение.ДвиженияПоСкладскимРегистрам, ЛОЖЬ)"
"И (НЕ ТаблицаТовары.Склад.ИспользоватьОрдернуюСхемуПриОтгрузке
И ТаблицаТовары.Номенклатура.ТипНоменклатуры В"
Что в этом плохого?
Ну вот представьте - начали вы проведение документа пока склад ещё был не ордерным, а в процессе проведения кто-то его поменял на ордерный...
Или тип номенклатуры сменил с "товара" на "услугу". Он это сможет влёгкую сделать. А вы потом не сможете доказать что "когда я нажимал кнопку всё было хорошо". Все проверки, включая конечно "обработку проверки заполнения" документ пройдёт ещё до изменения данных.
Кроме того - есть прекрасная возможность в один регистр записать данные исходя из информации что склад ордерный, а в другой - нет, и всё это в одной транзакции. Это и есть так называемое "фантомное чтение", которое, как мы помним, для "READ COMMITED SNAPSHOT" вполне возможно.
Наверное не нужно лишний раз писать что найти и устранить подобные ошибки очень и очень сложно. Ещё труднее понять что же являлось причиной подобного поведения системы.
"Эти справочники защищены от изменений, не так всё просто" - напрашивается комментарий. На самом деле всё просто. Распределенная база, полный обмен. Задано небольшое число элементов в транзакции. В каждом справочнике есть код:
Процедура ПередЗаписью(Отказ)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
Который отключает все эти проверки. В удаленной базе они могли быть пройдены, но на тот момент в ней не было актуальной копии документов, или вообще не было и не будет этих документов.
Для корректной работы проведения документа нужно чтобы на все считываемые неявным соединением таблицы накладывались разделяемые блокировки.
К слову, реально это нужно только для проведения - в других случаях необходимость что-то блокировать сомнительна.
Вывод 5: В типовых конфигурациях не учитывается необходимость блокировать записи таблиц с которыми происходит неявное соединение при проведении
Что делать я думаю понятно? Если есть параллельная работа в базе - реально параллельная, то по-хорошему при проведении документа нужно накладывать ещё кучу разделяемых блокировок, которые помогут избежать подобных ситуаций.
Почему на это не особенно обращают внимание? Да потому что эти ошибки видны только в случае реальной параллельной работы большого количества пользователей, да и то крайне редки. Если у вас много пользователей в базе 1С, у вас скорее всего наберётся несколько куч проблем более актуальных, чем описанный выше кейс. Ну а у кого всё хорошо и все вопросы решены скорее всего уже правильно расставлены блокировки, и такие крупные компании редко работают на типовых базах. Но вообще знать о таком "поведении" платформы нужно, как и учитывать его в проектах, которые ориентированы на большое количество параллельно работающих пользователей. Ведь пока мы (1С-ники) не научимся корректно работать с системами, в которых 1000+ активных пользователей и обеспечивать в них согласованность данных SAP нам на рынке не подвинуть.
P.S. Всё описанное выше "не баг а фича", вполне нормально документировано на ИТС, который всё равно никто не читает на котором вы можете детально ознакомиться с описанием работы управляемых блокировок "от Вендора" https://its.1c.ru/db/v8314doc#bookmark:dev:TI000000535
P.P.S. Если это прочитают коллеги из 1С - тут нет "камня в огород типовых" или в сторону реализации управляемых блокировок (хотя конечно хм...). Из статьи выше должно по задумке стать очевидным, что прикладным разработчикам очень не хватает возможности выбирать уровень изоляции с которым будет начинаться транзакция (в т.ч. неявная). READ COMMITED для проведения документов списания товара это слишком лайтово. Всех косяков, с этим связанных, не выловить.