Что такое транзакция записи документа в 1с

от admin

Вы не умеете работать с транзакциями

Заголовок вышел броским, но накипело. Сразу скажу, что речь пойдет об 1С. Дорогие 1С-ники, вы не умеете работать с транзакциями и не понимаете что такое исключения. К такому выводу я пришел, просматривая большое количество кода на 1С, рождаемого в дебрях отечественного энтерпрайза. В типовых конфигурациях с этим все достаточно хорошо, но ужасающее количество заказного кода написано некомпетентно с точки зрения работы с базой данных. Вы когда-нибудь видели у себя ошибку «В данной транзакции уже происходили ошибки»? Если да — то заголовок статьи относится и к вам. Давайте под катом разберемся, наконец, что такое транзакции и как правильно с ними обращаться, работая с 1С.

Почему надо бить тревогу

Для начала, давайте разберемся, что же такое представляет собой ошибка «В данной транзакции уже происходили ошибки». Это, на самом деле, предельно простая штука: вы пытаетесь работать с базой данных внутри уже откаченной (отмененной) транзакции. Например, где-то был вызван метод ОтменитьТранзакцию, а вы пытаетесь ее зафиксировать.

Почему это плохо? Потому что данная ошибка ничего не говорит вам о том, где на самом деле случилась проблема. Когда в саппорт от пользователя приходит скриншот с таким текстом, а в особенности для серверного кода, с которым интерактивно не работает человек — это… Хотел написать «критичная ошибка», но подумал, что это buzzword, на который уже никто не обращает внимания…. Это задница. Это ошибка программирования. Это не случайный сбой. Это косяк, который надо немедленно переделывать. Потому что, когда у вас фоновые процессы сервера встанут ночью и компания начнет стремительно терять деньги, то «В данной транзакции уже происходили ошибки» это последнее, что вы захотите увидеть в диагностических логах.

Есть, конечно, вероятность, что технологический журнал сервера (он ведь у вас включен в продакшене, да?) как-то поможет диагностировать проблему, но я сейчас навскидку не могу придумать вариант — как именно в нем найти реальную причину указанной ошибки. А реальная причина одна — программист Вася получил исключение внутри транзакции и решил, что один раз — не карабас «подумаешь, ошибка, пойдем дальше».

Что такое транзакции в 1С

Неловко писать про азбучные истины, но, видимо, немножго придется. Транзакции в 1С — это то же самое, что транзакции в СУБД. Это не какие-то особенные «1С-ные» транзакции, это и есть транзакции в СУБД. Согласно общей идее транзакций, они могут либо выполниться целиком, либо не выполниться совсем. Все изменения в таблицах базы данных, выполненные внутри транзакции, могут быть разом отменены, как будто ничего не было.

Далее, нужно понимать, что в 1С не поддерживаются вложенные транзакции. Собственно говоря, они не поддерживаются не «в 1С», а вообще не поддерживаются. По-крайней мере, теми СУБД, с которыми умеет работать 1С. Вложенных транзакций, например, нет в MS SQL и Postgres. Каждый «вложенный» вызов НачатьТранзакцию просто увеличивает счетчик транзакций, а каждый вызов «ЗафиксироватьТранзакцию» — уменьшает этот счетчик. Данное поведение описано в множестве книжек и статей, но выводы из этого поведения, видимо, разобраны недостаточно. Строго говоря, в SQL есть т.н. SAVEPOINT, но 1С их не использует, да и вещь это достаточно специфичная.

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

На самом деле, нет. Мне совершенно не хочется дублировать примеры на английском только ради того, чтобы потешить любителей холиваров и священных войн.

Вы же наверняка пишете такой код, да? Приведенный пример кода содержит ошибки. Как минимум, три. Знаете какие? Про первую я скажу сразу, она связана с объектными блокировками и не имеет отношения непосредственно к транзакциям. Про вторую — чуть позже. Третья ошибка — это deadlock, который возникнет при параллельном исполнении этого кода, но это тема для отдельной статьи, ее рассматривать сейчас не будем, дабы не усложнять код. Ключевое слово для гугления: deadlock управляемые блокировки.

Обратите внимание, простой ведь код. Такого в ваших 1С-системах просто вагон. И он содержит сразу, как минимум, 3 ошибки. Задумайтесь на досуге, сколько ошибок есть в более сложных сценариях работы с транзакциями, написанных вашими программистами 1С 🙂

Объектные блокировки

Итак, первая ошибка. В 1С существуют объектные блокировки, так называемые «оптимистические» и «пессимистические». Кто придумал термин, не знаю, убил бы :). Совершенно невозможно запомнить, какая из них за что отвечает. Подробно про них написано здесь и здесь, а также в прочей IT-литературе общего назначения.

Суть проблемы в том, что в указанном примере кода изменяется объект базы данных, но в другом сеансе может сидеть интерактивный пользователь (или соседний фоновый поток), который тоже будет менять этот объект. Здесь один из вас может получить ошибку «запись была изменена или удалена». Если это произойдет в интерактивном сеансе, то пользователь почешет репу, ругнется и попробует переоткрыть форму. Если это произойдет в фоновом потоке, то вам придется искать это в логах. А журнал регистрации, как вы знаете, медленный, а ELK-стек для журналов 1С у нас в отрасли настраивают единицы… (мы, к слову, входим в число тех, кто настраивает и другим помогает настраивать :))

Короче говоря, это досадная ошибка и лучше, чтобы ее не было. Поэтому, в стандартах разработки четко написано, что перед изменением объектов необходимо ставить на них объектную блокировку методом «ОбъектСправочника.Заблокировать()«. Тогда параллельный сеанс (который тоже должен так поступить) не сможет начать операцию изменения и получит ожидаемый, управляемый отказ.

А теперь про транзакции

С первой ошибкой разобрались, давайте перейдем ко второй.

Если не предусмотреть проверку исключения в этом методе, то исключение (например, весьма вероятное на методе «Записать()») выбросит вас из данного метода без завершения транзакции. Исключение из метода «Записать» может быть выброшено по самым разным причинам, например, сработают какие-то прикладные проверки в бизнес-логике, или возникнет упомянутая выше объектная блокировка. Так или иначе, вторая ошибка гласит: код, начавший транзакцию, не несет ответственность за ее завершение.

Именно так я бы назвал эту проблему. В нашем статическом анализаторе кода 1С на базе SonarQube мы даже отдельно встроили такую диагностику. Сейчас я работаю над ее развитием, и фантазия программистов 1С, чей код попадает ко мне на анализ, порой приводит меня в шок и трепет…

Почему? Потому что выброшенное наверх исключение внутри транзакции в 90% случаев не даст эту транзакцию зафиксировать и приведет к ошибке. Следует понимать, что 1С автоматически откатывает незавершенную транзакцию только после возвращения из скриптового кода на уровень кода платформы. До тех пор, пока вы находитесь на уровне кода 1С, транзакция остается активной.

Поднимемся на уровень выше по стеку вызовов:

Смотрите, что получается. Наш проблемный метод вызывается откуда-то извне, выше по стеку. На уровне этого метода разработчик понятия не имеет — будут ли какие-то транзакции внутри метода ОченьПолезныйИВажныйКод или их не будет. А если будут — то будут ли они все завершены… Мы же все тут за мир и инкапсуляцию, верно? Автор метода «ВажныйКод» не должен думать про то, что именно происходит внутри вызываемого им метода. Того самого, в котором некорректно обрабатывается транзакция. В итоге, попытка поработать с базой данных после выброса исключения изнутри транзакции, с высокой вероятностью приведет к тому, что «В данной транзакции бла-бла…»

Размазывание транзакций по методам

Второе правило «транзакционно-безопасного» кода: счетчик ссылок транзакций в начале метода и в его конце должен иметь одно и то же значение. Нельзя начинать транзакцию в одном методе и завершать ее в другом. Из этого правила, наверное, можно найти исключения, но это будет какой-то низкоуровневый код, который пишут более компетентные люди. В общем случае так писать нельзя.

Выше — неприемлемый говнокод. Нельзя писать методы так, чтобы вызывающая сторона помнила и следила за возможными (или вероятными — как знать) транзакциями внутри других методов, которые она вызывает. Это нарушение инкапсуляции и разрастание спагетти-кода, который невозможно трассировать, сохраняя рассудок.

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

Пытаемся исправить код

Вернемся к исходному методу и попытаемся его починить. Сразу скажу, что объектную блокировку мы чинить пока не будем, просто, чтобы не усложнять код примера.

Первый подход типичного 1С-ника

Обычно программисты 1С знают, что при записи может быть выдано исключение. А еще они боятся исключений, поэтому стараются их все перехватывать. Например, вот так:

Ну как, стало лучше, да? Ведь теперь, возможные ошибки записи обрабатываются и даже логируются. Исключения больше не возникнут при записи объекта. И в логе даже видно — на каком объекте, не поленился, вывел в сообщение ссылку вместо лаконичного «Ошибка записи справочника», как это часто любят писать вечно торопящиеся разработчики. Иными словами, налицо забота о пользователе и рост компетенций.

Однако, опытный 1С-ник здесь скажет, что нет, лучше не стало. По сути ничего не поменялось, а может даже стало и хуже. В методе «Записать()» платформа 1С сама начнет транзакцию записи, и эта транзакция будет уже вложенной по отношению к нашей. И если в момент работы с базой данных 1С свою транзакцию откатит (например, будет выдано исключение бизнес-логики), то наша транзакция верхнего уровня все равно будет помечена как «испорченная» и ее нельзя будет зафиксировать. В итоге этот код так и останется проблемным, и при попытке фиксации выдаст «уже происходили ошибки».

А теперь представьте, что речь идет не о маленьком методе, а о глубоком стеке вызовов, где в самом низу кто-то взял и «выпустил» начатую транзакцию из своего метода. Верхнеуровневые процедуры могут и понятия не иметь, что кто-то там внизу начинал транзакции. В итоге, весь код валится с невнятной ошибкой, которую расследовать невозможно в принципе.

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

Методы работы с транзакциями в 1С

Не будет лишним напомнить, что вообще 1С предоставляет нам для работы с транзакциями. Это всем известные методы:

  • НачатьТранзакцию()
  • ЗафиксироватьТранзакцию()
  • ОтменитьТранзакцию()
  • ТранзакцияАктивна()

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

И есть интересная особенность. Методы выхода из транзакции (Зафиксировать и Отменить) выбрасывают исключения, если счетчик транзакций равен нулю. То есть, если вызвать один из них вне транзакции, то возникнет ошибка.

Как правильно пользоваться этими методами? Очень просто: надо прочитать сформулированное выше правило: код, начавший транзакцию, должен нести ответственность за ее завершение.

Как же соблюсти это правило? Давайте попробуем:

Выше мы уже поняли, что метод ДелаемЧтоТо — потенциально опасен. Он может выдать какое-то исключение, и транзакция «вылезет» наружу из нашего метода. Окей, добавим обработчик возможного исключения:

Отлично, мы поймали возникающую ошибку, но что с ней делать? Записать сообщение в лог? Ну, может быть, если код логирования ошибок должен быть именно на этом уровне и ошибку мы тут ждем. А если нет? Если мы не ожидали тут никаких ошибок? Тогда мы должны просто передать это исключение выше, пусть с ними разбирается другой слой архитектуры. Делается это оператором «ВызватьИсключение» без аргументов. В этих ваших джава-сиплюсплюсах это делается точно так же оператором throw.

Так, стоп… Если мы просто прокидываем исключение дальше, то зачем тут вообще нужна Попытка? А вот зачем: правило заставляет нас обеспечить завершение начатой нами транзакции.

Теперь, вроде бы, красиво. Однако, мы ведь помним, что не доверяем коду ДелаемЧтоТо(). Вдруг там внутри его автор не читал этой статьи, и не умеет работать с транзакциями? Вдруг он там взял, да и вызвал метод ОтменитьТранзакцию или наоборот, зафиксировал ее? Нам очень важно, чтобы обработчик исключения не породил нового исключения, иначе исходная ошибка будет потеряна и расследование проблем станет невозможным. А мы помним, что методы Зафиксировать и Отменить могут выдать исключение, если транзакция не существует. Здесь-то и пригождается метод ТранзакцияАктивна.

Финальный вариант

Наконец, мы можем написать правильный, «транзакционно-безопасный» вариант кода. Вот он:

**UPD: в комментариях предложен более безопасный вариант, когда ЗафиксироватьТранзакцию расположен внутри блока Попытка. Здесь приведен именно этот вариант, ранее Фиксация располагалась после блока Попытка-Исключение.

Постойте, но ведь не только «ОтменитьТранзакцию» может выдавать ошибки. Почему же тогда «ЗафиксироватьТранзакцию» не обернут в такое же условие с «ТранзакцияАктивна»? Опять же, по тому же самому правилу: код, начавший транзакцию, должен нести ответственность за ее завершение. Наша транзакция необязательно самая первая, она может быть вложенной. На нашем уровне абстракции мы обязаны заботиться только о нашей транзакции. Все прочие должны быть нам неинтересны. Они чужие, мы не должны нести за них ответственность. Именно НЕ ДОЛЖНЫ. Нельзя предпринимать попыток выяснения реального уровня счетчика транзакций. Это опять нарушит инкапсуляцию и приведет к «размазыванию» логики управления транзакциями. Мы проверили активность только в обработчике исключения и только для того, чтобы убедиться, что наш обработчик не породит нового исключения, «прячущего» старое.

Чек-лист рефакторинга

Давайте рассмотрим несколько наиболее распространенных ситуаций, требующих вмешательства в код.

Паттерн:

Обернуть в «безопасную» конструкцию с Попыткой, Проверкой активности и пробросом исключения.

Паттерн:

Анализ и Рефакторинг. Автор не понимал, что делает. Начинать вложенные транзакции можно безопасно. Не нужно проверять условие, нужно просто начать вложенную транзакцию. Ниже по модулю он наверняка еще там извращается с их фиксацией. Это гарантированный геморрой.

Примерно похожий вариант:

аналогично: фиксация транзакции по условию — это странно. Почему тут условие? Что, кто-то иной мог уже зафиксировать эту транзакцию? Повод для разбирательства.

Паттерн:

  1. ввести управляемую блокировку во избежание deadlock
  2. ввести вызов метода Заблокировать
  3. обернуть в «попытку», как показано выше

Паттерн:

Данная транзакция уже не завершится в случае исключения. Нет смысла продолжать цикл. Код требуется переписать, сверяясь с исходной задачей. Дополнительно обеспечить более информативное сообщение об ошибке.

В заключение

Я, как вы уже, наверное, догадались, отношусь к людям, любящим платформу 1С и разработку на ней. К платформе, разумеется, есть претензии, особенно в среде Highload, но в общем и целом, она позволяет недорого и быстро разрабатывать очень качественные корпоративные приложения. Давая из коробки и ORM, и GUI, и веб-интерфейс, и Reporting, и много чего еще. В комментариях на Хабре обычно пишут всякое высокомерное, так вот, ребята — основная проблема 1С, как экосистемы — это не платформа и не вендор. Это слишком низкий порог вхождения, который позволяет попадать в отрасль людям, не понимающим, что такое компьютер, база данных, клиент-сервер, сеть и всякое такое. 1С сделала разработку корпоративных приложений слишком легкой. Я за 20 минут могу написать на ней учетную систему для закупок/продаж с гибкими отчетами и веб-клиентом. После этого, мне несложно подумать о себе, что и на больших масштабах можно писать примерно так же. Как-то там 1С сама все внутри сделает, не знаю как, но наверное сделает. Напишу-ка я «НачатьТранзакцию()».

Читать:
Как оптимизировать left join oracle

И знаете — самое главное, что это прекрасно. Простота разработки в 1С позволяет моментально реализовывать бизнес-идеи и встраивать их в процессы компании. Потом всегда можно отрефакторить, главное понимать как. И если вдруг вам нужна помощь в аудите вашей «медленной 1С» — обращайтесь к специалистам по оптимизации. Она совсем не медленная.

Для чего используется НачатьТранзакцию и ЗавершитьТранзакцию?

Транзакция предназначена для записи в информационную базу согласованных изменений. Все изменения, внесенные в информационную базу после начала транзакции, будут затем либо целиком записаны , либо целиком отменены . Т.е. например при проведении документа выключается электричество, и документ оказывается проведен неполностью. Чтобы такого не происходило, как раз используется методы НачатьТранзакцию() — ЗафиксироватьТранзакцию(), которые помогут избежать логических нарушений в базе. Также их применяют, чтобы выполнить набор команд, при которых требуется блокировать систему от промежуточных изменений другими пользователями. Как вариант — набор команд должен быть выполнен полностью: т.е. или все команды или ни одной.

Транзакции 1С:Підприємство и SQL. Уровни изоляции и проблемы параллельного доступа.

Данная статья содержит, в большей мере, теоретические сведения, необходимые для понимания важности транзакций и блокировок 1С:Підприємство и СУБД и это отражается на производительности 1С:Підприємство. В статье популярно описана связь транзакций и блокировок через уровни изоляции и проблемы параллельного доступа.
Данная статья не несет практических советов по решению конкретных проблем, но является основой для понимания следующих статей, в которых описываются шаги по оптимизации и улучшению производительности 1С:Підприємство, связанную с транзакциями и блокировками.

Table of Contents

Производительность напрямую связана с ТРАНЗАКЦИЯМИ 1С:Підприємство

«Конфликт блокировок при выполнении транзакции:
Microsoft OLE DB Provider for SQL Server: Lock request time out period exceeded .
HRESULT=80040E31, SQLSrvr: SQLSTATE=HYT00, state=34, Severity=10, native=1222, line=1»

Если 1С:Підприємство выдает ошибку примерно такого содержания то, Вы имеете дело с проблемами производительности, связанными с блокировками. Решение такого рода проблем не всегда тривиальные и требуют определенных специальных знаний по работе СУБД и 1С:Підприємство, которыми часто не владеют ни программисты 1С:Підприємство, ни системные администраторы. Следующий цикл статей как раз должен заполнить пробел этих знаний.

Транзакции 1С:Підприємство

Транзакция — это неделимая последовательность операций над данными. Она выполняется по принципу «все или ничего» и переводит базу данных
из одного целостного состояния в другое целостное состояние. Если по каким-либо причинам одно из действий транзакции невыполнимо или произошло какое-либо нарушение работы системы, база данных возвращается в то состояние, которое было до начала транзакции (происходит откат транзакции).

К механизму транзакций выдвигается ряд требований (известный под аббревиатурой ACID): Атомарность (Atomicity), Согласованность (Consistency), Изолированность (Isolation), Устойчивость (Durability)

Атомарность (Atomicity). Это требование заключается в том, что все данные, с которыми работает транзакция, должны быть либо подтверждены (commit), либо отменены (rollback). Не должно быть ситуации, когда часть изменений подтверждается, а часть – отменяется.

Для 1С:Підприємство свойства Атомарности транзакций обеспечивают логическую целостность данных. Например при записи документа, его данные шапки записываются в одну физическую таблицу СУБД, а данные табличной части в другую. Запись документа в транзакции дает гарантии что данные в обоих физических таблицах (шапок и табличных частей) будут согласованы (невозможна ситуация записи табличной части без шапки или наоборот).

Изолированность (Isolation). Транзакции должны выполняться автономно и независимо от других транзакций. При одновременном выполнении множества конкурирующих друг с другом транзакций, любое обновление определенной транзакции будет скрыто от остальных до тех пор, пока эта транзакция не будет зафиксирована. Существуют несколько уровней изоляции транзакций, которые позволяют выбрать наиболее оптимальное решение с точки зрения производительности и целостности данных. Основным методом реализации этих уровней являются блокировки, которые будут рассмотрены в этой статье.

Журнал транзакций (SQL Server)

Каждая база данных SQL Server имеет журнал транзакций, в котором фиксируются все изменения данных, произведенные в каждой транзакции. Если транзакция по каким-то причинам не завершилась (откатилась или прервалась), то SQL сервер по журналу транзакций отменяет все операции транзакции последовательно в обратном порядке. Это означает, что длительная транзакция на запись, будет долго и отменяться.

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

В зависимости от настроек базы данных (recovery model) журнал транзакций транзакции может обрезаться (удаляются данные старых транзакции) либо автоматически, либо вручную(recovery model=FULL). Иногда системный администратор забывает обрезать журнал и может возникнуть ошибка: «The transaction log for database is full«

Физически журнал транзакций СУБД MS SQL Server находится в файле .LDF (а файл данных — .MDF).

Транзакции в системе «1С:Підприємство»

Система «1С:Підприємство» осуществляет неявный вызов транзакций при выполнении любых действий, связанных с модификацией информации, хранящейся в базе данных. Например, все обработчики событий, расположенные в модулях объектов и наборов записей, связанные с модификацией данных базы данных, вызываются в транзакции.

Пример неявной транзакции: последовательность событий при проведении документа из формы

Транзакции 1С

Пример неявной транзакции: последовательность событий при проведении документа из формы

Практически определить что запись объекта 1С:Підприємство (например документа) происходи тв транзакции можно проведя следующий опыт: Попробовать провести и закрыть (нажать «ОК» в форме документа) новый документ заранее зная что он не проведется (например указав большое количество товара для списания). Так как остатки проверяются на этапе проведения документа, в обработчике «ОбработкаПроведения()», то к этому моменту сам документ уже должен быть записан в базу, так как запись документа происходит ранее между событиями «ПередЗаписью()» и «ПриЗаписи()». Но после появления сообщения об ошибке (отсутствие необходимого количества), Мы обнаружим что документ в базу не записан (останется флаг модифицированности «*» и в списке документ не появиться). Это происходит потому что транзакция после возникновения ошибки отменяется (откатывается «rollback»).

Использование явного вызова транзакций

Метод НачатьТранзакцию() позволяет открыть транзакцию. После этого все изменения информации базы данных, выполняемые последующими операторами, могут быть либо целиком приняты, либо целиком отвергнуты. Для принятия выполненных изменений используется метод ЗафиксироватьТранзакцию().
Для того чтобы отменить все изменения, выполнявшиеся в открытой транзакции, используется метод ОтменитьТранзакцию().

Степень изоляции транзакций определяется уровнями изоляции. Самый высокий уровень изоляции обеспечивает полную независимость транзакции от других, параллельно выполняющихся транзакций, но значительно понижается и степень параллельности работы — другим транзакциям приходится ожидать доступа к ресурсам, используемым в текущей транзакции. Самый низкий уровень изоляции наоборот: обеспечивает максимальную степень параллельной работы, что приводит к значительному влиянию других транзакций на текущую и появлению проблем параллельного доступа. В многопользовательских системах приходится искать компромисс между параллельностью работы (одновременному доступу к ресурсам) и уровнями изоляции транзакций. Стандарт языка SQL определяет уровни изоляции, установка которых предотвращает конкретные проблемы параллельного доступа.

Проблемы параллельного доступа

При параллельном выполнении транзакций возможны следующие проблемы:
потерянное обновление (англ. lost update) — при одновременном изменении одного блока данных разными транзакциями, одно из изменений теряется;
«грязное» чтение (англ. dirty read) — чтение данных, добавленных или изменённых транзакцией, которая впоследствии не подтвердится (откатится);
неповторяющееся чтение (англ. non-repeatable read) — при повторном чтении в рамках одной транзакции, ранее прочитанные данные оказываются изменёнными;
фантомное чтение (англ. phantom reads) — одна транзакция в ходе своего выполнения несколько раз выбирает множество строк по одним и тем же критериям. Другая транзакция в интервалах между этими выборками добавляет или удаляет строки, попадающие в критерии выборки первой транзакции, и успешно заканчивается. В результате получится, что одни и те же выборки в первой транзакции дают разные множества строк.

Рассмотрим ситуации, в которых возможно возникновение данных проблем:

Потерянное обновление

Предположим, имеются две транзакции, открытые различными приложениями, в которых выполнены следующие SQL-операторы:

Транзакция 1 Транзакция 2
SELECT f2 FROM tbl1 WHERE f1=1; SELECT f2 FROM tbl1 WHERE f1=1;
UPDATE tbl1 SET f2=20 WHERE f1=1;
UPDATE tbl1 SET f2=25 WHERE f1=1;

В Транзакции1 изменяется значение поля f1, а затем в Транзакции2 также изменяется значение этого поля в той же таблице. В результате изменение, выполненное первой транзакцией, будет потеряно.

Строго говоря, суть проблемы заключается в том, что неизвестно, какой результат будет зафиксирован
в конечном итоге.

«Грязное» чтение

Если предыдущая проблема возникает при записи данных, то «грязное» чтение возможно, когда одна транзакция пытается прочитать данные, с которыми работает другая параллельная транзакция.
Предположим, имеется две транзакции, открытые различными приложениями, в которых выполнены следующие SQL-операторы:

Транзакция 1 Транзакция 2
SELECT f2 FROM tbl1 WHERE f1=1;
UPDATE tbl1 SET f2=f2+1 WHERE f1=1;
SELECT f2 FROM tbl1 WHERE f1=1;
ROLLBACK WORK;

В Транзакции1 изменяется значение поля f1, а затем в Транзакции2 выбирается значение поля f2. После этого происходит откат Транзакции 1. В результате значение, полученное второй транзакцией, будет отличаться от значения, хранимого в базе данных.

Строго говоря, здесь тоже проблема именно в непредсказуемости результата. Т. е. вторая транзакция может «успеть» считать необновленное значение, а может не успеть и получить обновленное.

Неповторяющееся чтение

Предположим, имеются две транзакции, открытые различными приложениями, в которых выполнены следующие SQL-операторы:

Транзакция 1 Транзакция 2
SELECT f2 FROM tbl1 WHERE f1=1; SELECT f2 FROM tbl1 WHERE f1=1;
UPDATE tbl1 SET f2=f2+1 WHERE f1=1;
SELECT f2 FROM tbl1 WHERE f1=1;

В Транзакции2 выбирается значение поля f2, затем в Транзакции1 изменяется значение поля f2. При повторной попытке выбора значения из поля f2 в транзакции 1 будет получен другой результат. Эта ситуация особенно неприемлема, когда данные считываются с целью их частичного изменения и обратной записи в базу данных.

Фантомное чтение

Предположим, имеется две транзакции, открытые различными приложениями, в которых выполнены следующие SQL-операторы:

Транзакция 1 Транзакция 2
SELECT SUM(f2) FROM tbl1;
INSERT INTO tbl1 (f1,f2) VALUES (15,20);
SELECT SUM(f2) FROM tbl1;

В Транзакции2 выполняется SQL-оператор, использующий все значения поля f2. Затем в Транзакции1 выполняется вставка новой строки, приводящая к тому, что повторное выполнение SQL-оператора в транзакции 2 выдаст другой результат. Такая ситуация называется фантомной вставкой и является частным случаем неповторяющегося чтения.

Уровни изоляции транзакций

Уровень изоляции – это свойство транзакции, определяющее независимость работы транзакции от других, параллельно выполняющихся, транзакций.

Стандартом Введены следующие четыре уровня изоляции, применение которых предотвращает определенные проблемы параллельного доступа:
READ_UNCOMMITTED — нефиксированное чтение. Этот уровень изоляции решает проблему «потерянного обновления», но при этом возможно получение разных результатов для одинаковых запросов без учета фиксации транзакции (возможна проблема «грязного чтения»). Это самый низкий уровень изоляции, используемый в СУБД, который обеспечивает максимальную параллельность работы.
READ_COMMITTED — фиксированное чтение. Это уровень изоляции предотвращает проблему «грязного чтения», но позволяет получать разные результаты для одинаковых запросов в транзакции (сохраняется возможность «неповторяющегося чтения»);
REPEATABLE_READ — повторяющееся чтение. Это уровень изоляции решает проблему «неповторяющегося чтения». На этом уровне сохраняется возможно выполнение операторов INSERT, приводящих к конфликтной ситуации «фантомная вставка». Этот уровень целесообразно использовать, если на выполняющиеся SQL-операторы не влияет добавление новых строк;
SERIALIZABLE — последовательное выполнение. Третий уровень. Этот уровень гарантирует предотвращение всех описанных выше проблем параллельного доступа, но соответственно, наблюдается самая низкая степень параллелизма, так как обработка транзакций (с доступом к одним и тем же ресурсам) проводится только последовательно.

В MS SQL Server 2005 введен уровень изоляции SNAPSHOT – «снимок». Его суть заключается в том, что он не устанавливает блокировки (см. далее) на ресурсы, используемые транзакцией, а создает рабочие дубликаты («снимки») данных и работает с ними в ходе всей транзакции, тогда как исходные данные остаются неизменными до ее завершения. Параллельные транзакции, таким образом, работают как бы в «параллельной реальности». Такой механизм заимствован из СУБД Oracle. где в режиме READ_COMMITTED не устанавливается исключительную блокировку, а выдает другому сеансу копию (версию) данных до начала транзакции.
В «1С:Підприємство» данный уровень изоляции используется по умолчанию в версии 8.3.

Решение проблемы параллельного доступа транзакций и уровни изоляции в виде таблицы можно изобразить так («+» — проблема исключена):

Проблемы параллельного доступа и уровни изоляции Фантомное чтение Неповторяющееся чтение «Грязное» чтение Потерянное обновление
SERIALIZABLE + + + +
REPEATABLE_READ + + +
READ_COMMITTED + +
READ_UNCOMMITTED +

На уровне SQL сервера можно устанавливать уровень изоляции самостоятельно:
для всего сеанса, например директивой
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

для конкретного запроса с помощью конструкции WITH
SELECT Name FROM Contracts WITH SERIALIZABLE

Узнать уровень изоляции, установленный в текущей сессии
select transaction_isolation_level from sys.dm_exec_sessions
where session_id = @@spid

В автоматическом режиме (старый режим, который применялся в 8.0) управления блокировками данных используются уровни изоляции транзакций REPEATABLE_READ и SERIALIZABLE, обеспечиваемые системой управления базами данных. Эти уровни изоляции транзакций обеспечивают согласованное и целостное чтение данных, и от разработчика не требуется каких-либо дополнительных действий по управлению блокировками.

Управляемый режим блокировок (начиная с версии 8.1) позволяет повысить параллельность работы пользователей в клиент-серверном варианте работы за счет использования более низкого уровня изоляции транзакций базы данных (READ_COMMITTED); этот же уровень изоляции установлен по умолчанию
и в MS SQL сервере. При записи данных в транзакции объекты встроенного языка автоматически блокируют необходимые данные. Но при чтении разработчику требуется управлять блокировками данных в тех случаях, когда бизнес-логика требует согласованного и целостного чтения данных в транзакции.

Для версии 8.3 в управляемом режиме используется уровень изоляции READ_COMMITTED_SNAPSHOT.

Выводы

Транзакции — необходимый механизм СУБД, который активно используется в 1С:Підприємство. Для решения проблем параллельного доступа транзакции в СУБД могут выполняться с различными уровнями изоляции.

Используемый «1С:Підприємство» уровень изоляции READ_COMMITTED решает проблемы «Потерянное обновление» и «Грязное чтение»: измененные данные блокируются до конца транзакции и для чтения и для изменения (накладывается исключительная блокировка).

Уровень изоляции READ_COMMITTED не решает проблемы «Неповторяющееся чтение» и «Фантомное чтение». Чтобы решить эти проблемы необходимо использовать управляемые блокировки «1С:Підприємство», установленные программно.

В 8.3 используется более гибкий уровень изоляции READ_COMMITTED_SNAPSHOT.

Транзакции в 1С 8.3

Например, в одной транзакции может выполняться запись элемента справочника и запись информации в регистр сведений, со ссылкой на этот элемент справочника. В языке программирования 1С для работы с транзакциями используются следующие методы:НачатьТранзакцию(), ЗафиксироватьТранзакцию() и ОтменитьТранзакцию()

Рассмотрим конкретный пример. Пусть у нас есть справочник Товары и регистр сведений Цена с измерением Товар, в котором хранятся цены товаров. И мы хотим создать новый товар в справочнике и сразу записать его цену в рамках одной транзакции. То есть если при записи товара или цены произойдет ошибка, ни товар ни цена записаны не будут. Сделать это можно при помощи следующего кода:

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

А теперь давайте умышленно воспроизведем ошибку внутри транзакции. Для этого продублируем запись регистра сведений с тем же набором измерений (Товар) и периодом. Как известно платформа не даст записать в регистр сведений две записи с одинаковым набором измерений.

при попытке выполнить этот код возникает ошибка

Естественно, что ни нового товара ни записи с ценой мы после этого не обнаружим.
Если нам надо в зависимости от каких-то условий отменить транзакцию, необходимо воспользоваться методом ОтменитьТранзакцию().

Похожие статьи