Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
MUMPS СУБД. Практика применения и опыт программирования.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
4.5. ФУНКЦИЯ $INCREMENT 341
Для СУБД, основанных на SQL, также зачастую поддерживается функция автоинкрементного поля как упрощенный и специализирован­ный для прикладного применения механизм. Автоинкрементные поля ав­томатически совмещают в себе как безоткатное в транзакции получение следующего идентификатора, так и неявную систему именования таких идентификаторов. Те СУБД, которые поддерживают автоинкрементные поля, также должны поддерживать и механизм получения последнего сгенерированного текущим процессом такого значения, либо, если было автоматическое увеличение для нескольких таблиц, то получение полу­ченного номера для указанной таблицы.
Технически в MUMPS системах безоткатное действие выполняется в виде системной функции $increment(). Функции указывается перемен­ная и необязательный второй аргумент, на сколько следует увеличить значение. Если второй аргумент не указан, то функция увеличивает на
1. Если такой переменной не было или было нечисловой значение, то функция считает что было значение 0 и создает переменную с новым значением. В большинстве случаев разработчиками используется одно­аргументная форма функции.
Вышеприведенный код с применением этой функции уже может вы­глядеть так:
NextId()
n id l +^DATA s id=$increment(^DATA) l -^DATA q id
И уже такой вариант обеспечивает корректный сценарий получения следующего идентификатора для различных процессов, в том числе вы­полняющих откаты транзакций. Но, в силу того, что эта операция осо­бенная, MUMPS реализации не требуют выполнения отдельной специ­альной блокировки используемой переменной и выполняют корректный атомарный доступ с выполнением арифметических операций самостоя­тельно. Также корректным поэтому является такой код:
NextId()
q $increment(^DATA)
При выполнении функции $increment MUMPS реализация гаранти­рует, что, пока эта операция выполняется, ни один другой процесс не сможет изменить это значение даже если будет устанавливать блоки­ровки или также одновременно выполнит $increment. Во втором случае
342 ГЛАВА 4. КОНКУРЕНТНЫЙ ДОСТУП
система обеспечивает строго последовательное выполнение увеличения значения и оба процесса получат различные значения идентификаторов.
В техническом отношении функция $increment предусматривает яв­ное задание значения, на сколько следует увеличить значение перемен­ной, и, в том числе, это приращение может быть дробным, нулевым и отрицательным.
Важным моментом для более глубокого понимания работы функции $increment является то, что ее нельзя полноценно заменить на отклю­чение журналирования. В основную формулировку входит требование невозврата значения при откате. И, казалось бы, если разработчик от­ключит журналирование на период вычисления нового значения, то эта запись не попадет в журнал и откат транзакции не выполнит возврат для инкрементируемого значения.
С точки зрения работы прикладной системы такая замена может ока­заться корректной заменой, но с точки зрения работы сервера - нет. Если мы будем выполнять восстановление базы данных после сбоя, то часть данных сервер берет из бекапа, а часть данных дописывается из журна­ла в том, что касается изменений, еще не попавших в базу данных из журнала. Поэтому, если в журнале не будет записей об изменении ин­крементируемой переменной, то она может быть восстановлена на одно из предыдущих состояний по файлу бекапа, но счетчик не восстановится в корректное состояние соответствующее уже использованным номерам идентификаторов, содержащимся в журнале.
Поэтому, для корректного выполнения операции $increment, в жур­нал пишется специальная запись, которую игнорирует операция отката транзакции, но не игнорирует операция восстановления базы данных по журналу. И в запись пишется не предыдущее значение, а значение, ко­торое получено после изменения переменной.
Для тех MUMPS систем, которые используют автоматическую ре­пликацию по журналу, например Cach´e, также важно использовать имен­но функцию $increment, а не отключение журналирования, поскольку результат инкремента переменной также должен быть доставлен в реп­лицируемую базу.
Одним из важных выводов для разработчиков на М при использо­вании транзакций является то, что следует внимательно пересмотреть операции получения идентификатора и стратегии использования пере­менных для их хранения. Нужно иметь в виду также и тот факт, что сме­шивание операций set и $increment никак не пресекается самой СУБД. Если происходит их смешивание, то система при откате вернет значение, бывшее предыдущим для set, несмотря на то, что в транзакции также была использована и операция $increment.
4.6. ФУНКЦИЯ $BIT 343
И еще одним немаловажным фактом применения функции $increment является то, что она не входит в стандарт языка MUMPS и стандарт, хотя и предусматривает откат транзакций, но не предусматривает ме­ханизма безоткатных изменений. Сама функция $increment введена в различные реализации MUMPS систем их производителями самостоя­тельно и согласованно между собой, опираясь на наиболее приемлемые практики и методики.

4.6 Функция $BIT

При использовании битовых индексов в MUMPS системах важным для понимания работы системы и ее применения является дополнительный к механизму битовых функций механизм конкурентного доступа, выпол­ненный специально для них.
Битовый индекс представляет собой совокупность сегментов, каждый из которых это последовательность байт, и каждый бит в ней значим, может иметь значение либо "объект с идентификатором, равным номеру бита, существует" (1), либо "не существует" (0). Кроме того, применяется правило умолчания, что если бит находится за пределами реально физи­чески существующих байт, либо строка байт вообще не существует, то логически для битовых операций это эквивалентно последовательности нулей.
В техническом отношении битовые операции могут быть добавлены к MUMPS системе внешними по отношению к ней функциями в ди­намической библиотеке. Автору довелось участвовать в проектах, где использовался именно такой вариант. Разработки блестяще работали в режиме OLAP и имели некоторые непреодолимые недочеты в режиме OLTP.
Если операции с битами выполняются внешними по отношению к MUMPS системе средствами, то для записи в глобал остается использо­вать операцию set. Для MUMPS системы это операция полной переза­писи всего значения. Конечно, есть MUMPS системы в которых приме­няется дифференциальное журналирование, например как в MiniM, и в журнал записывается по возможности лишь изменение строки байт, а не вся строка, но в целом, вообще говоря, журналируется именно операция set.
В логическом отношении значимым изменением является изменение одного бита, а физически это для MUMPS системы целый большой пол­ноценный set. Это приводит к двум проблемам:
344 ГЛАВА 4. КОНКУРЕНТНЫЙ ДОСТУП
1. Для простановки одного бита требуется взять полное значение сег­мента, изменить в нем 1 бит и записать новое полное значение сегмента.
2. Для MUMPS системы видна лишь операция set.
Эти проблемы приводят к следующим последствиям: первая требует блокировать весь сегмент на время изменения, чтобы другой процесс не перезаписал значение сегмента ранее и его изменения не были утраче­ны. Поэтому возникает конфликт блокировок - хотя разным процессам требуется изменить разные биты, им необходимо использовать одно и то же имя блокировки. При высоконагруженной работе это приводит к уве­личению вероятности взаимоблокировок. Вторая проблема приводит к большому объему журналирования, хотя из значимых изменений - всего один бит.
Эти две проблемы структурно не являются характерными для какой­либо определенной архитектуры или типа СУБД. В случае применения битовых индексов в любой другой системе они также существуют. Мож­но увидеть в рекомендациях, в том числе и для других типов СУБД, рекомендации использовать битовые индексы лишь для хранилищ дан­ных, приближенных по своему режиму работы к режиму read-only, и рекомендации по возможности не использовать битовые индексы для задач класса OLTP.
В современных MUMPS системах, таких как Cach´e и MiniM, эти обе проблемы были решены на уровне СУБД введением двух дополнитель­ных механизмов, работающих при использовании битовых функций:
1. При изменении бита система выполняет эту операцию атомарно, с внутренней синхронизацией, не попадающей в множество блокиро­вок команды lock, и не удерживающейся до окончания транзакции.
2. При изменении бита система использует специальную запись в журнале.
В совокупности эти обе меры приводят к тому, что один единичный бит может быть проставлен независимо от других и при откате транзак­ции именно этот бит будет возвращен в предыдущее состояние, даже ес­ли другие процессы продолжают изменять битовую строку. Точно также значимые биты будут проставляться по отдельности при восстановлении из бекапа с дополнением по журналу.
Выполненные архитектурные меры по отношению к функции $bit снимают необходимость использовать блокировки индексных структур
4.7. ДЕДЛОКИ 345
при перестроении битмап индексов, хотя в примерах в целях методологии они могут присутствовать.
Кроме того, при использовании битовых индексов в современных ре­ализациях MUMPS систем также снимается рекомендация не использо­вать битмап индексы в OLTP задачах, а использовать по возможности только в OLAP задачах. В силу транзакционности таких битмап индек­сов они точно также применими в любых OLTP задачах, как и индексы других типов. Это дает разработчикам свободу выбора при построении качественно других прикладных систем.
Точно так же, как и в случае с функцией $increment, разработчи­ки должны разделить глобалы на те, к которым применяются операции $bit и те, к которым применяется прямое изменение другими формами команды set. В случае смешивания способов изменения байтовой строки MUMPS система будет журналировать именно использованную опера­цию вне зависимости от того, было ли изменение этой же строки иными способами.
Нужно отметить, что функции семейства $bit не являются частью стандарта MUMPS, соглашения принятые в одних системах, могут не поддерживаться в других. Этот функционал не входит в уровень пере­носимости. Кроме того, различные MUMPS системы могут использовать различные методы компрессии и кодирования битовых строк для умень­шения общего объема хранения. При переносе данных, таким образом, нельзя переносить битмап индексы как есть, их необходимо перестроить на целевой системе заново.

4.7 Дедлоки

Конкурентный доступ нескольких процессов к одним и тем же дан­ным содержит крупную неприятность, называемую отдельным термином мертвой блокировки. Алгоритмически эта проблема не является специ­фикой MUMPS систем или систем основанных на других архитектурах или методах, а характерна для конкурентного доступа как такового.
Дедлоки (deadlock), или мертвая блокировка - это явление, событие или состояние двух или более процессов, один из которых, заблокировав ресурс 1, ожидает освобождения ресурса 2, но ресурс 2 в свою очередь заблокирован другим процессом, и он ожидает освобождения ресурса 1. Цепочка взаимоблокировок может быть более длинной, с вовлечением нескольких процессов.
Для иллюстрации примера приведем код на MUMPS, условно вос­производящий ситуацию с обновлением нескольких (для простоты двух)
346 ГЛАВА 4. КОНКУРЕНТНЫЙ ДОСТУП
объектов, имеющих два простых атрибута, имеющих всего два значения. Функция action выполняет обновление как строки данных
^zAug("data")
так и двух индексов
^zAug("prop1") ^zAug("prop2")
Функция run1 выполняет имитацию изменения объектов.
Основной задачей примера является поиск решения проблемы с deadlock, возникающим при работе с битовыми индексами, в который вероятность конфликта доступа при обновлении индексов возрастает в тысячи раз. Поэтому в приведенном примере воспроизведения deadlock блокировка узла данных не выполняется.
run1()
n id,prop1,prop2 f d . w "." . s id=$r(2),prop1=$r(2),prop2=$r(2) . d action1(id,prop1,prop2) q
action1(id,prop1,prop2)
n oldprop1,oldprop2 ; s oldprop1=$lg($G(^zAug("data",id)),1) s oldprop2=$lg($G(^zAug("data",id)),2) ; l:oldprop1’="" +^zAug("prop1",oldprop1,id) l:oldprop2’="" +^zAug("prop2",oldprop2,id) l +^zAug("prop1",prop1,id) l +^zAug("prop2",prop2,id) ; k:oldprop1’="" ^zAug("prop1",oldprop1,id) k:oldprop2’="" ^zAug("prop2",oldprop2,id) ; s ^zAug("prop1",prop1,id)="" s ^zAug("prop2",prop2,id)="" ; s ^zAug("data",id)=$lb(prop1,prop2) h .5 ; l:oldprop1’="" -^zAug("prop1",oldprop1,id) l:oldprop2’="" -^zAug("prop2",oldprop2,id) l -^zAug("prop1",prop1,id) l -^zAug("prop2",prop2,id) q
4.7. ДЕДЛОКИ 347
После запуска
d run1
в двух процессах они в течении некоторого времени уверенно встают в deadlock. Будем использовать этот код как базовый для модернизации с целью нахождения решения.
Для возникновения ситуации мертвой блокировки необходимо воз-
никновение четырех условий:
1. Процессы требуют предоставления им права монопольного управ­ления ресурсами, которые им выделяются (условие взаимоисклю­чения).
2. Процессы удерживают за собой ресурсы, уже выделенные им, ожи­дая в то же время выделения дополнительных ресурсов (условие ожидания ресурсов).
3. Ресурсы нельзя отобрать у процессов, удерживающих их, пока эти ресурсы не будут использованы для завершения работы (условие неперераспределяемости).
4. Существует кольцевая цепь процессов, в которой каждый процесс удерживает за собой один или более ресурсов, требующихся сле­дующему процессу цепи (условие кругового ожидания).
В задачах конкурентного доступа к базе данных все эти условия присутствуют. Для решения проблемы необходимо разорвать одно (или более) из перечисленных условий. Приведенное далее решение проблемы использует изменение программистом выполнения процесса так, чтобы разорвать второе условие. Решение состоит в разбиении имен ресур­сов на группы, с тем, чтобы блокировать имена ресурсов одной группы списком (или одновременно), и имена групп ресурсов упорядочить. При блокировке списком отпадает наращивание блокировок той же группы в дополнение к тем, которые могут ожидаться другими процессами.
Общий метод выглядит примерно так: разбиваем ресурсы на иерархи­ческие группы. Назовем их группа1, группа2, ... группаN. При этом вво­дится дисциплина блокирования: ресурсы группы n+1 могут быть бло­кированы только если нет других блокировок группы n+1 или старше и если есть блокировка имен группы n или младше. Группой ноль бу­дем считать отсутствие блокировок. В рамках одной группы блокировку выполняем списком.
348 ГЛАВА 4. КОНКУРЕНТНЫЙ ДОСТУП
Группы следует организовать таким образом, чтобы имена блокиро­вок группы n+1 были известны после блокирования имен группы n. В примере используется инкрементальная блокировка списком чтобы удер­жать имевшиеся ранее блокировки и добавить к ним новые.
Текст примера для воспроизведения / тестов:
run3() n id,prop1,prop2 f d . w "." . s id=$r(2),prop1=$r(2),prop2=$r(2) . d action3bit(id,prop1,prop2) q ; то же самое что action3 но имитируем битовые сегменты
action3bit(id,prop1,prop2)
n str,old,oldprop1,oldprop2 s str=$na(^zAug("prop1",prop1,1))_","_
$na(^zAug("prop2",prop2,1)) l +^zAug("data",id) s oldprop1=$lg(^zAug("data",id),1) s oldprop2=$lg(^zAug("data",id),2) ; это на случай запуска теста без созданных данных s:oldprop1="" oldprop1=" " s:oldprop2="" oldprop2=" " s old=$na(^zAug("prop1",oldprop1,1))_","_
$na(^zAug("prop2",oldprop2,1)) x "l +("_old_","_str_")" ; ничего не значит, это имитация. ; реально тут битовая операция сброса бита x "s ("_str_")=""""" s ^zAug("data",id)=$lb(prop1,prop2) h 0.4 ; тоже ничего не значит, это имитация. ; реально тут битовая операция установки бита x "s ("_str_")=""""" x "l -("_old_","_str_")" l -^zAug("data",id) q ; обычный обратный индекс
action3(id,prop1,prop2)
n str,old,oldprop1,oldprop2 s str=$na(^zAug("prop1",prop1,id))_","_
$na(^zAug("prop2",prop2,id)) l +^zAug("data",id) s oldprop1=$lg(^zAug("data",id),1) s oldprop2=$lg(^zAug("data",id),2) ; это на случай запуска теста без созданных данных s:oldprop1="" oldprop1=" "
4.7. ДЕДЛОКИ 349
s:oldprop2="" oldprop2=" " s old=$na(^zAug("prop1",oldprop1,id))_","_
$na(^zAug("prop2",oldprop2,id)) x "l +("_old_","_str_")" x "k "_old ; ну просто совпало так s ^zAug("data",id)=$lb(prop1,prop2) h 0.4 x "s ("_str_")=""""" ; тут тоже удачно совпало x "l -("_old_","_str_")" l -^zAug("data",id) q
Нетрудно убедиться, что замена блокировок списком на последова­тельные тут же приводит к ситуации deadlock в течении довольно корот­кого времени.
В приведенном примере две группы:
1. ˆzAug("data",...) - узел данных
2. ˆzAug("prop1",...) и ˆzAug("prop2",...) - узлы индексов Некоторый условный пример с более осмысленным содержанием: пред-
положим, что есть объект документ с объектами - стр оки документа. Для операций с объектом делим операции и имена блокировок на 4 группы:
1. Сохранения документа, вход - идентификатор документа, дальше передаются имена для группы2 (имена индексов) и группы3 (иден­тификаторы объектов - строк документа).
2. Индексы документа.
3. Объекты строк документа, вход - идентификатор объекта строки, дальше передаются имена для группы4 (имена индексов по стро­кам).
4. Индексы строк.
Ставим блокировку на идентификатор документа, выполняем опера­ции с объектом документа. По его содержанию определяем имена бло­кировок для индексов документа и объектов строк документа. Блокируя индексы, выполняем изменение индексов. Блокируя строки документа, выполняем операции со строками. По блокированным строкам можем определить имена индексов. Блокируем индексы, выполняем обновление индексов.
Также следует отметить, что при использовании в битовых операциях функций семейства $bit в современных версиях Cach´e и MiniM проблема deadlock автоматически снимается, поскольку операция
350 ГЛАВА 4. КОНКУРЕНТНЫЙ ДОСТУП
s $bit(^data,n)=0 s $bit(^data,n)=1
корректно отрабатывается без необходимости блокирования всего сег­мента, и корректно работает откат транзакции. При использовании в качестве реализации битовых операций функций семейства $bit полу­чаем выигрыш как в более свободной параллельной работе процессов, так и в существенном снижении объемов журналирования изменений индексных сегментов.
Другим решением проблемы дедлоков является разрыв четвертого условия, кольцевого ожидания. Для этого выполнение процессов плани­руется таким образом, чтобы захват ресурсов выполнялся в строго опре­деленном отношении условных имен этих ресурсов. Для этого вводится правило упорядочивания ресурсов и дисциплина доступа, при которой захват ресурса должен производиться строго в определенном направле­нии этого упорядочения.
Для вышеприведенного примера, для исключения взаимоблокировки, нужно построить такой же список ресурсов, но блокировать не единым списком, а провести сортировку имен в некотором обще-оговоренном порядке и выполнять блокирование в порядке полученной очередности.
Большинство программных систем, в которых возможно возникно­вение взаимоблокировок, обычно применяют именно разрыв четвертого условия и упорядочивают захват ресурсов. При этом разработчики про­рабатывают ход выполнения для каждой из операций и при необходимо­сти корректируют алгоритмы так, чтобы сохранить порядок блокирова­ний.
Для тех ситуаций, где применение методов разрыва условий неприме­нимо или применение решений слишком трудоемко, разработчики долж­ны для избежания останова системы вводить компенсатор для второго условия (условие ожидания). Это выполняется указанием времени ожи­дания. Если по истечении определенного времени ресурс не был предо­ставлен, то процесс должен принять решение об откате выполняемого действия и либо счесть обнаруженное состояние непреодолимым, либо попытаться повторить операцию снова.
В MUMPS системах время ожидания указывается в одном из пара­метров команды lock:
lock +name:timeout
Разработчики всегда должны проверять состояние системной пере­менной $TEST, чтобы проверить, была ли блокировка с таймаутом успеш­ной или нет, с помошью команд IF или ELSE, например: