Транзакцыі ў глабалах InterSystems IRIS

Транзакцыі ў глабалах InterSystems IRISСКБД InterSystems IRIS падтрымлівае цікаўныя структуры для захоўвання дадзеных – глабалы. Па сутнасці гэта шматузроўневыя ключы з рознымі дадатковымі плюшкамі ў выглядзе транзакцый, хуткіх функцый для абыходу дрэў дадзеных, блакіровак і сваёй мовы ObjectScript.

Падрабязней пра глабалы ў цыкле артыкулаў «Глобалы — мячы-кладзенцы для захоўвання дадзеных»:

Дрэвы. Частка 1
Дрэвы. Частка 2
Разрэджаныя масівы. Частка 3

Мне стала цікава як рэалізаваны транзакцыі ў глабалах, якія там ёсць асаблівасці. Бо гэта зусім іншая структура для захоўвання дадзеных, чым усім звыклыя табліцы. Нашмат больш нізкаўзроўневая.

Як вядома з тэорыі рэляцыйных баз дадзеных добрая рэалізацыя транзакцый павінна задавальняць патрабаванням ACID:

A - Atomic (атамарнасць). Запісваюцца ўсе змены зробленыя ў транзакцыі ці ўвогуле ніякіх.

З - Consistency (ўзгодненасць). Пасля завяршэння транзакцыі лагічны стан БД павінна быць унутрана несупярэчлівым. Шмат у чым гэтае патрабаванне дакранаецца праграміста, але ў выпадку SQL-баз дадзеных яно дакранаецца таксама вонкавых ключоў.

I - Isolate (ізаляванасць). Паралельна якія выконваюцца транзакцыі не павінны аказваць уплыў сябар на сябра.

D - Durable (даўгавечнасць). Пасля паспяховага завяршэння транзакцыі праблемы на ніжніх узроўнях (збой па харчаванні, напрыклад) не павінны ўплываць на дадзеныя змененыя транзакцыяй.

Глобалы - гэта нерэляцыйныя структуры дадзеных. Яны ствараліся для звышхуткай працы на вельмі абмежаваным жалезе. Давайце разбяромся ў рэалізацыі транзакцый у глабалах з дапамогай афіцыйнага docker-выявы IRIS.

Для падтрымкі транзакцый у IRIS выкарыстоўваюцца каманды: ТСТАРТ, TCOMMIT, TROLLBACK.

1. Атамарнасць

Лягчэй за ўсё праверыць атамарнасць. Правяраем з кансолі базы дадзеных.

Kill ^a
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TCOMMIT

Потым які робіцца выснова:

Write ^a(1), “ ”, ^a(2), “ ”, ^a(3)

Атрымаем:

1 2 3

Усё ў парадку. Атамарнасць выканана: усе змены запісаліся.

Ускладнім задачу, увядзем памылку і паглядзім як захаваецца транзакцыя, часткова ці ўвогуле ніяк.

Яшчэ раз праверым атамарнасць:

Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3

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

docker kill my-iris

Гэтая каманда практычна эквівалентная гвалтоўнаму выключэнню сілкавання, бо адпраўляе сігнал неадкладнага прыпынку працэсу SIGKILL.

Можа транзакцыя захавалася часткова?

WRITE ^a(1), ^a(2), ^a(3)
^
<UNDEFINED> ^a(1)

- Не, не захавалася.

Выпрабуем каманду адкату:

Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TROLLBACK

WRITE ^a(1), ^a(2), ^a(3)
^
<UNDEFINED> ^a(1)

Таксама нічога не захавалася.

2. Узгодненасць

Паколькі ў базах на глабалах ключы робяцца таксама на глабалах (нагадаю, што глабал — гэта больш нізкаўзроўневая структура для захоўвання дадзеных, чым рэляцыйная табліца), для выканання патрабавання ўзгодненасці трэба змену ключа ўключаць у тую ж транзакцыю, што і змену глабалу.

Напрыклад у нас ёсць глобал ^person, у якім мы захоўваем персаналіі і ў якасці ключа мы выкарыстоўваем ІНАЎ.

^person(1234567, ‘firstname’) = ‘Sergey’
^person(1234567, ‘lastname’) = ‘Kamenev’
^person(1234567, ‘phone’) = ‘+74995555555
...

Для таго, каб мець хуткі пошук па прозвішчы і імі мы зрабілі ключ ^index.

^index(‘Kamenev’, ‘Sergey’, 1234567) = 1

Для таго, каб база была ўзгоднена мы павінны дадаваць персаналію так:

TSTART
^person(1234567, ‘firstname’) = ‘Sergey’
^person(1234567, ‘lastname’) = ‘Kamenev’
^person(1234567, ‘phone’) = ‘+74995555555
^index(‘Kamenev’, ‘Sergey’, 1234567) = 1
TCOMMIT

Адпаведна, пры выдаленні мы таксама павінны выкарыстоўваць транзакцыю:

TSTART
Kill ^person(1234567)
ZKill ^index(‘Kamenev’, ‘Sergey’, 1234567)
TCOMMIT

Іншымі словамі выкананне патрабавання ўзгодненасці ляжыць цалкам на плячах праграміста. Але калі гаворка ідзе аб глабалах - гэта нармальна, у сілу іх нізкаўзроўневай прыроды.

3. Ізаляванасць

Вось тут пачынаюцца нетры. Шмат карыстальнікаў адначасова працуюць над адной і той жа базай, змяняюць адны і тыя ж дадзеныя.

Сітуацыя параўнальная з тым, калі шмат карыстачоў адначасова працуюць з адным і тым жа рэпазітаром з кодам і спрабуюць адначасова ў яго каміціць змены ў шматлікіх файлах адразу.

База дадзеных павінна гэта ўсё разрульваць у рэальным часе. Улічваючы, што ў сур'ёзных кампаніях ёсць нават спецыяльны чалавек, які адказвае за кантроль версій (за зліццё галінак, вырашэнне канфліктаў і да т.п), а БД усё гэта павінна рабіць у рэальным часе, то становіцца відавочнай складанасць задачы і правільнасць праектавання базы дадзеных кода, які яе абслугоўвае.

База дадзеных не можа зразумець сэнс дзеянняў выкананых карыстальнікамі, каб не дапусціць канфліктаў, калі яны працуюць над аднымі і тымі ж дадзенымі. Яна можа толькі адмяніць адну транзакцыю супярэчлівую іншы ці выканаць іх паслядоўна.

Яшчэ адна праблема, што падчас выканання транзакцыі (да комміта), стан базы можа быць няўзгодненым, таму пажадана, каб у іншых транзакцый не было доступу да няўзгодненага стану базы дадзеных, што дасягаецца ў рэляцыйных БД многімі спосабамі: стварэнне снэпшотаў, шматверсійнасцю радкоў і да т.п.

Пры паралельным выкананні транзакцый нам важна, каб яны не перашкаджалі адна адной. Гэта і ёсць уласцівасць ізаляванасці.

SQL вызначае 4 узроўня ізаляванасці:

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • СЕРЫЯЛІЗУЕМАЯ

Разгледзім кожны ўзровень у асобнасці. Выдаткі на рэалізацыю кожнага ўзроўню растуць ці ледзь не экспанентна.

READ UNCOMMITTED - гэта самы нізкі ўзровень ізаляванасці, але пры гэтым самы хуткасны. Транзакцыі могуць чытаць змены ўнесеныя адна адной.

READ COMMITTED - Гэта наступны ўзровень ізаляцыі, які з'яўляецца кампрамісам. Транзакцыі не могуць чытаць змены ўнесеныя адна адной да коміта, але могуць чытаць любыя змены ўнесеныя пасля коміта.

Калі ў нас ёсць доўгая транзакцыя Т1, на працягу якой прайшлі коміты ў транзакцыях Т2, Т3… Тn, якія працавалі з тымі ж дадзенымі што і Т1, то пры запыце даных у Т1 мы будзем кожны раз атрымліваць розны вынік. Гэты феномен называецца непаўторнае чытанне.

REPEATABLE READ - у гэтым узроўні ізаляцыі ў нас няма феномену непаўторнага чытання, за кошт таго, што для кожнага запыту на чытанне дадзеных ствараецца здымак дадзеных выніку і пры паўторным выкарыстанні ў гэтай жа транзакцыі выкарыстоўваецца дадзеныя са здымка. Аднак у гэтым узроўні ізаляцыі магчымае чытанне фантомных дадзеных. Маюцца на ўвазе чытанне новых радкоў, якія былі дададзены паралельнымі зафіксаванымі транзакцыямі.

СЕРЫЯЛІЗУЕМАЯ - Самы высокі ўзровень ізаляцыі. Ён характарызуецца тым, што дадзеныя якія-небудзь выявай выкарыстоўваныя ў транзакцыі (чытанне або змена) становяцца даступнымі іншым транзакцыям толькі пасля завяршэння першай транзакцыі.

Для пачатку разбяромся ці ёсць ізаляцыя аперацый у транзакцыі ад асноўнага патоку. Адкрыем 2 вокны тэрмінала.

Kill ^t

Write ^t(1)
2

TSTART
Set ^t(1)=2

Ізаляцыі няма. Адзін паток бачыць, што робіць другі, хто адкрыў транзакцыю.

Паглядзім ці бачаць транзакцыі розных плыняў тое, што адбываецца ўнутры іх.

Адкрыем 2 вокны тэрмінала і адкрыем 2 транзакцыі паралельна.

kill ^t
TSTART
Write ^t(1)
3

TSTART
Set ^t(1)=3

Паралельныя транзакцыі бачаць дадзеныя адна адной. Такім чынам мы атрымалі самы просты, але і самы хуткі ўзровень ізаляцыі READ UNCOMMITED.

У прынцыпе гэта можна было чакаць для глабалаў, для якіх хуткадзейнасць заўсёды ставілася ў раздзел кута.

Як жа быць, калі нам спатрэбіцца больш высокі ўзровень ізаляцыі ў аперацыях на глабалах?

Тут трэба задумацца навошта ўвогуле патрэбны ўзроўні ізаляцыі і як яны працуюць.

Самы высокі ўзровень ізаляцыі SERIALIZE азначае, што вынік паралельна выкананых транзакцый эквівалентны іх паслядоўнаму выкананню, што гарантуе адсутнасць калізій.

Такое мы можам зрабіць з дапамогай пісьменных блакіровак у ObjectScript, якія маюць масу разнастайных спосабаў ужывання: можна рабіць звычайную, інкрыментную, множную блакіроўку камандай Блакаванні.

Ніжэйшыя ўзроўні ізаляцыі - гэта кампрамісы закліканыя павялічыць хуткасць працы базы дадзеных.

Паглядзім як мы можам дабіцца розных узроўняў ізаляцыі з дапамогай блакіровак.

Гэты аператар дазваляе браць не толькі эксклюзіўныя блакіроўкі, патрэбныя для змены дадзеных, але так званыя shared, якія могуць браць раўналежна адразу некалькі струменяў, калі ім трэба чытаць дадзеныя, якія не павінны быць зменены іншымі працэсамі падчас чытанняў.

Падрабязней пра двухфазны метад блакіровак на рускай і англійскай мовах:

Двухфазная блакіроўка
Two-phase locking

Складанасць у тым, што падчас транзакцыі стан базы можа быць няўзгодненым, аднак гэтыя няўзгодненыя дадзеныя бачныя іншым працэсам. Як гэтага пазьбегнуць?

Мы зробім з дапамогай блакіровак такія вокны бачнасці, у якіх стан базы будзе ўзгоднена. І ўсе звароты да такіх вокнаў бачнасці ўзгодненага стану будзе кантралявацца блакіроўкамі.

Shared-блакіроўкі адных і тых жа дадзеных шматразовыя - іх могуць узяць некалькі працэсаў. Гэта блакіроўкі забараняюць іншым працэсам змяняць дадзеныя, г.зн. яны выкарыстоўваюцца для фармавання вокнаў узгодненага стану БД.

Эксклюзіўныя блакіроўкі выкарыстоўваюцца для змен дадзеных - такую ​​блакіроўку можа ўзяць толькі адзін працэс. Эксклюзіўнае блакаванне можа ўзяць:

  1. Любы працэс, калі дадзеныя вольныя
  2. Толькі той працэс, які мае на гэтыя дадзеныя shared-блакіроўку і першы запытаў эксклюзіўную блакіроўку.

Транзакцыі ў глабалах InterSystems IRIS

Чым ужо акно бачнасці, тым даўжэй яго даводзіцца чакаць іншым працэсам, але тым узгоднены можа быць стан БД у ім.

READ_COMMITED - сутнасць гэтага ўзроўню, што мы бачым толькі закаммічаныя дадзеныя з іншых патокаў. Калі дадзеныя ў іншай транзакцыі яшчэ не закаммічаны, то мы бачым іх старую версію.

Гэта дазваляе нам распаралеліць працу замест чакання вызвалення блакіроўкі.

Без спецыяльных хітрыкаў мы не зможам бачыць старую версію дадзеных у IRIS, таму нам давядзецца абысціся блакіроўкамі.

Адпаведна нам давядзецца з дапамогай shared блакіровак дазволіць чытанне дадзеных толькі ў моманты ўзгодненасці.

Дапусцім у нас ёсць база карыстальнікаў ^person, якія пераводзяць адзін аднаму грошы.

Момант перакладу ад персоны 123 да персоны 242:

LOCK +^person(123), +^person(242)
Set ^person(123, amount) = ^person(123, amount) - amount
Set ^person(242, amount) = ^person(242, amount) + amount
LOCK -^person(123), -^person(242)

Момант запыту колькасці грошай у персоны 123 перад спісаннем павінен суправаджацца эксклюзіўным блакаваннем (па змаўчанні):

LOCK +^person(123)
Write ^person(123)

А калі трэба паказаць стан рахунку ў асабістым кабінеце, то можна выкарыстоўваць shared блакаванне ці ўвогуле яе не выкарыстоўваць:

LOCK +^person(123)#”S”
Write ^person(123)

Аднак, калі дапусціць, што аперацыі працы з БД выконваюцца практычна імгненна (нагадаю, што глабалы - гэта нашмат больш нізкаўзроўневая структура, чым рэляцыйная табліца), то неабходнасць гэтага ўзроўню падае.

REPEATABLE READ - У гэтым узроўні ізаляцыі дапускаецца, што могуць быць некалькі чытанняў дадзеных, якія могуць быць зменены паралельнымі транзакцыямі.

Адпаведна нам давядзецца ставіць shared блакаванне на чытанне дадзеных, якія мы змяняем і эксклюзіўныя блакіроўкі на дадзеныя якія мы змяняем.

Балазе аператар LOCK дазваляе ў адным аператары дэталёва пералічыць усе неабходныя блакіроўкі, якіх можа быць вельмі шмат.

LOCK +^person(123, amount)#”S”
чтение ^person(123, amount)

іншыя аперацыі (у гэты час раўналежныя струмені спрабуюць змяніць ^person(123, amount), але не могуць)

LOCK +^person(123, amount)
изменение ^person(123, amount)
LOCK -^person(123, amount)

чтение ^person(123, amount)
LOCK -^person(123, amount)#”S”

Пры пераліку блакіровак праз коску яны бяруцца паслядоўна, а калі зрабіць так:

LOCK +(^person(123),^person(242))

то яны бяруцца атамарна ўсё адразу.

СЕРЫЯЛІЗАЦЫЯ - нам давядзецца выставіць блакіроўкі так, каб у канчатковым выніку ўсе транзакцыі, якія маюць агульныя дадзеныя выконваліся паслядоўна. Для гэтага падыходу большасць блакіровак павінны быць эксклюзіўнымі і брацца на самыя маленькія вобласці глабала для прадукцыйнасці.

Калі ж казаць пра спісанні сродкаў у глабале ^person, то для яго прымальны толькі ўзровень ізаляцыі SERIALIZE, бо грошы павінны марнавацца строга паслядоўна, інакш магчыма выдаткаваць адну і тую ж суму некалькі разоў.

4. Доўгачасовасць

Я праводзіў тэсты з жорсткім высяканнем кантэйнера пасродкам

docker kill my-iris

База іх пераносіла добра. Праблем не было выяўлена.

Заключэнне

Для глабалаў у InterSystems IRIS ёсць падтрымка транзакцый. Яны сапраўды атамарныя, надзейныя. Для забеспячэння ж узгодненасці БД на глабалах неабходны намаганні праграміста і выкарыстанне транзакцый, бо ў ёй няма складаных убудаваных канструкцый тыпу вонкавых ключоў.

Узровень ізаляцыі ў глабалаў без выкарыстання блакіровак - гэта READ UNCOMMITED, а пры выкарыстанні блакіровак можна яго забяспечыць аж да ўзроўню SERIALIZE.

Карэктнасць і хуткасць працы транзакцый на глобалах вельмі моцна залежыць ад майстэрства праграміста: чым шырэйшае выкарыстоўваюцца shared-блакаванні пры чытанні, тым вышэй узровень ізаляцыі, а чым вузейшае бяруцца эксклюзіўныя блакаванні, тым больш хуткадзейнасць.

Крыніца: habr.com

Дадаць каментар