Ова страница је машински преведена. Прочитајте енглески оригинал. English

Трансакције у Firebird-у: ACID, нивои изолације, мртве блокаде и решавање сукоба при ажурирању

Alexey Kovyazin, уз помоћ Vlad-а Khorsun-а и Dmitry-ја Kuzmenko-а, 08-АПР-2019

Садржај:

Да ли је неопходно знати како трансакције раде?

Вероватно јесте, јер је појам трансакција једноставан, многи програмери потцењују важност правилног коришћења трансакција у Firebird-у. Међутим, тек након што стекнете темељно разумевање како трансакције раде, можете схватити многе мистериозне ствари повезане са перформансама, као што су изненадна успоравања базе података (повезана са чишћењем прекомерних верзија записа које настају због лошег управљања трансакцијама).

Уопштено, појам трансакције примењује се на било који динамички систем који прелази из једног стања у друго. На пример, класичан пример трансакције је пренос новца са једног рачуна на други. Обично то изгледа отприлике овако:

Code

Почетак   --- пренос новца са рачуна 1 на рачун 2
 --смањење рачуна 1
 --повећање рачуна 2
Крај - потврђивање трансакције

Пример се своди на чињеницу да новац мора нестати са рачуна 1 и појавити се на рачуну 2 истовремено, иначе ће у систему бити или вишка новца или необјашњивог мањка новца неко време.

Са становишта база података, трансакција се обично дефинише као група операција изведених над базом података која се посматра као независна од других трансакција. Са моје тачке гледишта, ова дефиниција није ни боља ни лошија од других, али, као и свака дефиниција, има мало смисла без познавања стварног унутрашњег функционисања и логике СУБП-а.

Сматра се да трансакција у бази података мора испунити такозване ACID захтеве

Code
A - Атомичност
С - Конзистентност
I - Изолација
D - Трајност

Многи програмери апликација за базе података су толико инспирисани овим акронимом да често користе аргументе као што су “немаш D у ACID-у” када је реч о поређењу различитих СУБП-а (што обично одмах прати “не занима ме шта ти мислиш”).

Заправо, све је прилично једноставно - ACID је скуп захтева у вези са имплементацијом трансакције у одређеном СУБП-у, неки од њих су веома строги (на пример, D - наравно, трајност је важна!) док су неки мање строги - када погледамо нивое изолације трансакција, видећемо да изолација може варирати.

Зато не вреди покушавати одмах разумети шта овај акроним буквално значи. Уместо тога, испитаћемо логику како СУБП-ови (а посебно трансакције) раде и посматрати ACID са становишта “како је направљено” уместо “шта значи”.

Пошто су аспекти трансакција сложени, потребна нам је графичка представа - врста графикона, да бисмо приказали рад и интеракцију трансакција. Користећи ове графиконе, моћи ћемо да изградимо логичко приповедање и уђемо у детаље како трансакције раде.

Пре свега, увешћемо временску линију јер се трансакције развијају у времену. Временска линија ће бити означена онако како нам треба - не требају нам ни секунде ни минути, већ кључни кораци интеракције између трансакција:

Затим ћемо додати трансакцију на ову временску линију - нацртаћемо је у облику правоугаоника чије ће странице одговарати почетку и крају трансакције. Пошто су све трансакције у Firebird-у нумерисане, такође ћемо навести број трансакције.

Дакле, графикон приказује трансакцију број 11 која је почела у тренутку t3 и завршила се у тренутку t10. Постоје два начина да се трансакција заврши - COMMIT, тј. примена свих измена направљених у оквиру трансакције, и ROLLBACK, тј. отказивање свих измена направљених у оквиру трансакције. Приказаћемо начин на који се трансакција завршава на следећи начин:

Да бисмо могли да наставимо, мораћемо да наведемо различите параметре трансакција на овим графиконима и навешћемо их у доњем левом углу правоугаоника који представља одговарајућу трансакцију - овај пример показује да трансакција #11 има snapshot ниво изолације.

Када кажемо да “трансакција X убацује податке” или “трансакција Y чита ово и оно” - то је формално нетачно, јер треба рећи “измене су направљене у оквиру трансакције X”. Само SQL изјаве могу читати или убацивати податке, па ако је то важно за приповедање, приказаћемо ове изјаве унутар правоугаоника трансакције:

У овом примеру имамо INSERT операцију за табелу T1, поље i1, вредност 100 - ова операција се изводи у оквиру трансакције #11 и потврђена је.

Такође, понекад ћемо морати да прикажемо резултат операције, на пример, у следећем примеру:

Овај пример показује следеће:

  1. Трансакција #11 са параметром нивоа изолације постављеним на snapshot (нивои изолације ће бити размотрени касније, овде је приказано само да би се добила потпуна слика) почиње у тренутку t3
  2. Операција INSERT INTO T1(i1) values (100) која убацује вредност 100 у поље i1 табеле t1 почиње у тренутку t5 И завршава се у тренутку t7
  3. Операција SELECT i1 from T1 која враћа вредност i1 једнаку 100 почиње у тренутку t8
  4. Трансакција #11 се завршава COMMIT изјавом, тј. измене направљене трансакцијом #11 се потврђују у бази података

Дакле, уз помоћ графикона трансакција, можемо детаљно описати шта се дешава у бази података и сазнати како трансакције раде.

Сада када имамо графиконе трансакција, да видимо шта ACID акроним заиста значи.

Атомичност

Атомичност значи да се или све операције које чине трансакцију изводе или ниједна од њих: “све или ништа”. Изгледа као лако, али онда детаљи излазе на видело.

Прво, СУБП (не само Firebird већ скоро сви) има 2 врсте атомичности: атомичност на нивоу изјаве и атомичност на нивоу групе изјава у оквиру трансакције.

Атомичност на нивоу изјаве значи да се UPDATET1 SETX=1 WHEREY=2 изјава увек или успешно изврши или не.

Атомичност на нивоу групе изјава функционише другачије (овде ћемо користити псеудокод да означимо када је трансакција покренута и потврђена):

Code
Покрени трансакцију 11
INSERT ..100
INSERT ..200
INSERT ..300
Потврди 11

Отприлике овако ће то изгледати на графикону:

То значи да су све три INSERT изјаве успешно извршене и измене направљене њима се потврђују у тренутку када се трансакција #11 потврди.

Питање које често постављам на радионицама када је реч о трансакцијама - да ли ће COMMIT изјава бити успешно извршена за трансакцију 11 ако INSERT INTO..300 изазове изузетак:

Значајан део публике увек одговара да COMMIT изјава неће бити успешно извршена! (Занимљиво је да ће то у неким другим СУБП-овима изазвати враћање трансакције уназад!)

Међутим, то није тачно - само покрените isql и изведите експеримент са било којом базом података (isql има једноставну и директну имплементацију операција, без “погађања” за корисника).

Ствар је у томе што је атомичност на нивоу група изјава обезбеђена потврђивањем трансакција питање пословне логике. Програмер апликације мора одлучити да ли трансакција треба да буде потврђена у случају изузетка у трећој INSERT изјави или не. Ако пословна логика омогућава потврђивање резултата, COMMIT изјава се лако може извршити.

Дакле, захтев атомарности у ACID-у је захтев да DBMS може да потврди или врати резултате групе изјава извршених у оквиру једне трансакције. Одлука да се потврди или врати зависи од пословне логике коју треба да имплементирате.

И да још једном нагласимо - иако атомарност трансакције за групу изјава значи могућност потврђивања или враћања целе групе без обзира на резултате (а избор зависи од пословне логике), атомарност једне изјаве је гарантована имплементацијом DBMS-а, тј. немогуће је извршити једну изјаву (на пример, UPDATE) „непотпуно“ (неатомарно).

Конзистентност

Конзистентност значи да подаци у бази података не представљају контрадикције. Наравно, овде можемо видети цело поље за спекулације јер „шта уопште значи ’не представља контрадикције’“?

Обично се издвајају два нивоа конзистентности:

  1. Ниво базе података где конзистентност значи усклађеност података са ограничењима базе података, као што су Primary, Unique и Foreign кључеви, Checks. Овај ниво конзистентности је обезбеђен чињеницом да ограничења базе података неће омогућити уметање података који не одговарају ограничењима: нпр. CHECK(x>0) неће дозволити да се негативан број уметне у одговарајуће поље.
  2. Ниво пословне логике где конзистентност обезбеђује програмер апликације уз помоћ алата које нуди DBMS, као што су трансакције.

Како трансакције помажу да се обезбеди конзистентност на нивоу пословне логике? Прилично једноставно - ако узмемо пример преноса новца, програмер мора да обезбеди да се све измене врате у случају изузетка и коришћење трансакције му у томе помаже.

Code
Starttransaction
Смањи износ новца на рачуну 1…. Успех
Повећај га на рачуну 2… Неуспех
Rollback ---- у случају изузетка!

Другим речима, програмер мора да напише код на такав начин да се подаци врате у случају изузетка и тако се конзистентност података очува са становишта пословне логике.

На овај начин, захтев конзистентности у акрониму ACID значи да је неопходно да DBMS има могућност да одржи конзистентност података уз помоћ механизма трансакција.

Изолација

Захтев изолације трансакција произилази из потребе да се гарантује резултат скупа операција без обзира на редослед којим се извршавају.

Једноставно речено, свака трансакција мора да се изврши са истим резултатом без обзира на то да ли су друге трансакције истовремено активне.

Механизам трансакција треба да обезбеди конзистентност на нивоу пословне логике, али такође треба да заштити трансакције од привремених непотврђених података који се могу појавити током процеса извршавања истовремених трансакција.

У пракси то изгледа овако:

Видимо трансакцију #11 покренуту у тренутку t2, у оквиру које се уметање врши у табелу у тренутку t3-t5. Трансакција #11 није потврђена одмах након уметања, већ остаје активна до тренутка t8.

Истовремено, трансакција #12 је покренута и извршава SELECT изјаву за записе у табели у коју су уметнути подаци трансакције #11. Прва SELECT изјава се извршава у тренутку t6, када је операција уметања већ завршена, али ова изјава враћа празан резултат јер трансакција #12 не може да види непотврђене податке из других трансакција.

Трансакција #11 је потврђена у тренутку t8 и SELECT изјава у оквиру трансакције #12 се извршава у тренутку t9. Враћа резултат једнак 100 јер су подаци створени у оквиру трансакције #11 сада потврђени (и јер је ниво изолације трансакције #12 read committed, али о томе ћемо касније).

Овај пример је сасвим довољан да илуструје захтев изолације - за разлику од атомарности и конзистентности, изолација је имплементирана као строга правила која се називају нивои изолације и свака трансакција мора да има параметар који поставља ниво изолације са којим ради.

Трајност

Концепт трајности омогућава програмеру да се у потпуности ослони на чињеницу да ће се подаци створени у оквиру потврђене трансакције одмах појавити у бази података и да неће нестати из ње (без експлицитних изјава које их бришу или мењају, наравно) без обзира на то шта се даље догоди.

Као што видите, захтев трајности је само здрав разум - тешко да би се неко сложио да користи систем из којег подаци могу изненада нестати.

ACID: резиме

ACID значи захтеве како трансакције морају да раде:

  • Атомарност
    • Изјаве су увек атомарне
    • Групе изјава могу бити учињене атомарним уз помоћ трансакција
  • Конзистентност
    • Два нивоа конзистентности: ограничења базе података и пословна логика
  • Изолација
    • Обезбеђена механизмом трансакција уз помоћ нивоа изолације који су им постављени
  • Трајност
    • Сви потврђени подаци постају трајни

Као што видите, све је прилично логично. У пракси, главну потешкоћу представљају нивои изолације, па хајде да детаљно видимо како они раде.

Ниво изолације трансакције дефинише које потврђене податке ова трансакција може да види.

Постоје нивои изолације који се конвенционално називају стандардним. Они су описани у ANSI SQL стандарду (различите ревизије). Колико ја знам, не постоји ниједан DBMS где су имплементирани тачно онако како су описани у стандарду, али никога то не брине јер стварни механизми трансакција у одређеним DBMS-овима имају све потребне опције за имплементацију пословне логике.

Класичну дефиницију нивоа изолације можете пронаћи у „ A Critique of ANSI SQL Isolation Levels

За оне који су прочитали овај чланак, ево табеле која упоређује класичне нивое изолације са сличнима у Firebird-у. Наравно, кореспонденција није директна јер нивои изолације у Firebird-у, као и у другим DBMS-овима, не испуњавају 100% ANSI SQL дефиниције, али су веома слични њима.

ANSI нивои изолације Ниво изолације у Firebird-у
Read Uncommitted n/a
Read Committed Read Committed
Repeatable Read Snapshot
Serializable Snapshot table stability

Као и сваки други DBMS, Firebird има своје посебности у имплементацији изолације. Сада ћемо се фокусирати на то како нивои изолације раде у Firebird-у, уместо на то колико добро испуњавају стандард.

Snapshot ниво изолације

Snapshot ниво изолације је био први у оригиналном коду InterBase-а и остаје подразумевани за Firebird core API и алате (на пример, isql.exe). Ово може бити разлог зашто га је најлакше разумети.

Snapshot изолује трансакцију од било каквих измена направљених од тренутка њеног покретања.

Погледајмо дијаграм трансакција испод: приказује трансакцију #10 покренуту са snapshot нивоом изолације. У оквиру ове трансакције, извршава се неколико SELECT изјава за табелу T1 која у овом примеру нема записа.

Истовремена трансакција #15 покренута након почетка трансакције #10 умеће податке у табелу T1 и ова трансакција се завршава COMMIT изјавом у тренутку t9, тј. подаци су потврђени у бази података у овом тренутку и доступни су за изјаве из других трансакција.

Међутим, изјава у трансакцији #10 извршена у тренутку t10 (тј. након што је трансакција #15 потврђена) не види уметнуте податке јер snapshot ниво изолације дозвољава да види само потврђене податке уметнуте или измењене ПРЕ ПОЧЕТКА трансакције #10.

Дакле, Snapshot ниво изолације вам омогућава да радите са базом података као да је замрзнута у тренутку када трансакција почиње. Обично је неопходан за изградњу сложених извештаја заснованих на подацима који се брзо мењају: snapshot се користи да би се избегла ситуација када је први део извештаја заснован на једним подацима, а последњи део на другим подацима.

Међутим, ова одлична функција има своју цену - када касније будемо испитивали како је изолација имплементирана у Firebird-у, видећете да започињање веома дугих трансакција са нивоом изолације snapshot резултира прекомерним верзијама записа и нижим перформансама.

Ниво изолације Read Committed

Трансакција са нивоом изолације read committed може да види потврђене податке других трансакција које су потврђене док је она активна (за разлику од случаја са нивоом snapshot, када можете видети само податке потврђене пре тренутка када трансакција почиње).

Покажимо како ниво изолације read committed функционише користећи следећи дијаграм:

Он приказује пример практично идентичан претходном: две конкурентне трансакције од којих једна редовно чита податке из табеле T1 док друга убацује и потврђује податке.

За разлику од случаја са нивоом изолације snapshot, трансакција #10 у овом примеру види податке које је убацила и потврдила трансакција #15.

Овај пример нам даје представу о утицају нивоа изолације read committed: изјаве унутар трансакције са овим нивоом изолације могу видети податке потврђене пре тренутка када је одговарајућа изјава извршена.

Следећи дијаграм приказује пример где две конкурентне трансакције #11 и #18 мењају податке.

Имајте на уму да трансакција #11 почиње пре почетка трансакције #14 која чита податке, док трансакција #18 почиње после ње, али то не утиче на резултат: ако су подаци потврђени, конкурентна трансакција са нивоом изолације read committed их може видети.

Ова могућност чини ниво изолације read committed природним избором за оне SQL изјаве које се редовно извршавају како би се приказало најновије стање базе података (на пример, како би се приказале најновије поруџбине).

Део посвећен сакупљању смећа ће показати да су read committed трансакције са read-only модификатором у Firebird-у до верзије 4 најбољи избор за “бесконачне” read трансакције јер се започињу као пре-потврђене.

Ниво изолације Snapshot table stability

Могуће је направити причу о нивоу изолације snapshot table stability, који је пандан стандардном Serializable нивоу изолације, или веома кратком или прилично дугачком и детаљном.

Кратка верзија приче је следећа: овај ниво је потпуно сличан нивоу snapshot са додатним закључавањем табеле (табела мора бити експлицитно наведена у параметрима трансакције) за писање и читање. То значи да је могуће започети трансакцију која ће у потпуности заузети наведену табелу и било које друге трансакције ће добити грешке приступа.

Другим речима, трансакција са нивоом изолације snapshot table stability ће заправо ставити све упите ка наведеној табели у ред чекања. У ствари, само читања у редовним трансакцијама ће бити обављена ван реда (као и обично) док ће сви остали режими формирати ред (то зависи од интеракције, наравно).

Ако се имплементира без опреза, то може изазвати закључавања и немогућност рада са базом података, због чега се програмери апликација за Firebird базу података могу плашити коришћења овог нивоа изолације.

Међутим, ако се правилно имплементира, Serializable ниво изолације омогућава лако формирање редова и секвенцијалне измене у записима базе података, што може бити веома корисно за имплементацију бројача, секвенцијалних бројева докумената и других сличних објеката.

Да бисмо правилно описали како формирати ред уз помоћ трансакције са нивоом изолације snapshot table stability, мораћемо да погледамо још један параметар трансакције: wait/nowait - а затим се вратити на пример реда.

Раније смо испитивали такав начин интеракције између трансакција да се подаци мењају унутар једне трансакције и читају унутар друге трансакције.

Међутим, у пракси се често дешава да различите трансакције покушавају да промене исте податке и пошто се само један резултат чува у бази података, конкурентна трансакција ће добити поруку о конфликту - у ствари, изузетак који ће прекинути (и отказати) извршење оне одређене изјаве која покушава да промени већ измењене податке.

Опција wait дефинише како трансакција треба да реагује на конфликт ажурирања. Постоје три начина за конфигурисање ове опције:

  1. Wait (без параметара) = чекај док се конкурентна трансакција не заврши
  2. Wait Timeout N sec = чекај док се конкурентна трансакција не заврши, али не више од N секунди
  3. Nowait - не чекај док се конкурентна трансакција не заврши

Имајте на уму да је опција wait овде наведена у псеудокоду док називи могу бити другачији у API-ју и у специфичним компонентама, иако значење остаје исто.

Погледајмо детаљно шта се дешава у случају конфликата ажурирања са различитим варијантама опције wait.

Wait

Дакле, замислимо две истовремено активне трансакције (#11 и #14) унутар којих се извршава UPDATE изјава која мора да промени један исти запис у једној истој табели T1.

Трансакција #14 се покреће са опцијом wait (ако користите isql за репродукцију примера, wait је подразумевано подешен).

UPDATE изјава у трансакцији #11 почиње у тренутку t3 и завршава се у тренутку t5, али трансакција још није потврђена - тј. COMMIT изјава не постоји до тренутка t6.

Дијаграм испод приказује ову ситуацију:

UPDATE изјава се такође извршава у трансакцији #14 и покушава да ажурира исти запис у истој табели, али почиње касније - приближно у тренутку t4.

Пошто постоји конфликт ажурирања са ажурирањем из трансакције #11 и wait је наведен у трансакцији #14, UPDATE изјава ће чекати док се конфликтна трансакција #11 не заврши.

Ако трансакција #11 траје довољно дуго, UPDATE изјава у трансакцији #14 ће изгледати замрзнута са становишта корисника који посматра извршење ове изјаве.

Ако репродукујете ову ситуацију уз помоћ два isql.exe, следећа слика приказује тренутак када друга трансакција (тачније трансакција где конкурентна UPDATE изјава почиње касније - то је трансакција #14 у нашем примеру) чека док се прва трансакција не заврши (то је трансакција #11 у нашем примеру).

Након што се COMMIT изјава изврши у трансакцији #11, трансакција #14 која је чекала на њу ће одмах бити обавештена и конфликтно ажурирање ће се завршити изузетком.

Испод можете видети пример такве поруке о грешци (број конкурентне трансакције се не поклапа са нашим примером јер бројеви трансакција почињу од почетка у свакој бази података и затим се само увећавају док се ресетују тек након backup/restore-а):

Code
SQL> update T1 set i1 = 2 where i1=1;
Statement failed, SQLSTATE = 40001
deadlock
-update conflicts with concurrent update
-concurrent transaction number is 19
SQL>

Обратите пажњу на реч “deadlock” у поруци о грешци - сада заправо не постоји deadlock према његовој класичној дефиницији. Уместо тога, постоји конфликт ажурирања, али програмери Firebird-а не мењају поруку о грешци јер се користи више од 35 година. Бавићемо се правим “класичним” deadlock-ом касније.

Дакле, испитали смо ситуацију када се трансакција са конкурентном UPDATE изјавом завршава COMMIT изјавом. Хајде сада да узмемо сличну ситуацију, али када је она враћена назад - можете је видети на дијаграму испод:

Ситуација је потпуно слична претходној - две UPDATE изјаве покушавају да ажурирају један исти запис, али конкурентна трансакција #20 се овог пута завршава враћањем назад и измене унутар трансакције #15 се чувају у бази података без грешке као резултат.

Дакле, опција wait омогућава организовање пословне логике ажурирања на такав начин да конфликтна ажурирања бесконачно чекају у реду надајући се до последњег тренутка да ће се трансакција која је у конфликту са њима завршити ROLLBACK изјавом.

Да ли ова тактика увек има смисла? Наравно, зависи од имплементације пословне логике, али Firebird нуди и друге опције за решавање конфликата при ажурирању уз помоћ опције чекања.

Чекање са временским ограничењем

Пре свега, можда је добра идеја ограничити време чекања - уместо бесконачног чекања у случају конфликта, можете ограничити време чекања тако што ћете одредити временско ограничење за опцију чекања.

У isql.exe такав параметар се одређује помоћу следеће изјаве:

Code
SET TRANSACTION WAIT LOCK TIMEOUT N;

Где је N време (у секундама) које ће конкурентна трансакција чекати да се конфликт реши.

Више детаља о изјавама за контролу трансакција можете пронаћи у Firebird Language Reference. Имајте на уму да могу постојати различити начини за одређивање временског ограничења у одређеним драјверима или компонентама за приступ (обично, уз помоћ API параметра).

Пример у isql можете видети на слици испод:

Проучимо уз помоћ дијаграма трансакција како трансакције интерагују ако одредите временско ограничење за опцију чекања.

Дакле, ситуација је иста - две конкурентне трансакције #11 и #14 у оквиру којих се извршава UPDATE изјава која покушава да ажурира исти запис у табели T1.

Међутим, у овом случају, изјава у оквиру трансакције #14 чека или док се трансакција #11 не заврши или док не истекне одређено временско ограничење (3 секунде) - шта год се прво догоди.

Временско ограничење истиче раније у овом примеру, изјава се завршава изузетком:

Code
SQL> update T1 set i1=6 where i1=1;
Statement failed, SQLSTATE = 40001
lock time-out on wait transaction
-deadlock
-update conflicts with concurrent update
-concurrent transaction number is 40

Имајте на уму да је “deadlock” поново у поруци о грешци, али то и даље није “прави” deadlock.

Сходно томе, ситуација је слична оној са опцијом чекања, али је ограничена временским ограничењем - ако одређено временско ограничење истекне раније него што се конкурентна трансакција заврши.

Одређивање опције чекања са временским ограничењем може бити добро решење за имплементацију пословне логике ако сигурно знате да су све трансакције писања прилично кратке (на пример, не дуже од 1-2 секунде).

Nowait

Веома је лако објаснити шта је Nowait са формалне тачке гледишта - то је чекање са нултим временским ограничењем. Ако одредите nowait у трансакцијама, конфликтна ажурирања ће одмах подићи изузетак.

У овом случају, поново имамо конкурентне трансакције #11 и #14 (nowait) где се извршавају конкурентне UPDATE изјаве. Изјава у оквиру трансакције са опцијом nowait не чека када види конкурентно ажурирање, већ одмах подиже следећи изузетак у тренутку свог ажурирања (само је број трансакције другачији):

Code
SQL> update T1 set i1=5 where i1=1;
Statement failed, SQLSTATE = 40001
lock conflict on no wait transaction
-deadlock
-update conflicts with concurrent update
-concurrent transaction number is 50
SQL>

Ево како то изгледа у примеру са два isql алата:

Имајте на уму да nowait трансакцији није стало када и како се трансакција са конкурентном UPDATE изјавом завршава - било да је то COMMIT или ROLLBACK изјава, изузетак се ипак подиже.

Са становишта пословне логике, nowait трансакција може бити згодна ако сигурно знате да конкурентно ажурирање мора несумњиво резултирати отказивањем радњи тренутне изјаве.

Многи Firebird драјвери користе опцију nowait као подразумевану вредност и, док многи програмери не знају да је могуће поставити мање строг ниво за решавање конфликата при ажурирању (на пример, wait lock timeout 1), њихове апликације (а понекад и корисници) пате од непотребних грешака због конфликата.

Пошто је кључна реч “deadlock” присутна у сваком изузетку везаном за конфликте при ажурирању, многи програмери апликација су сигурни да је то оно што је прави deadlock заправо (неки чак мисле да је неки Very Dead имао улогу у овој грешци).

Истовремено, ако погледамо конфигурациони фајл firebird.conf, видећемо параметар DeadlockTimeout тамо (подразумевано је 10 секунди), а ако погледамо заглавље излаза fb_lock_print алата, такође ћемо видети параметар “Deadlock scans”.

Ствар је у томе да је “прави deadlock” могућ у Firebird-у и кључна реч “deadlock” која се појављује у свим изузетцима везаним за конфликте при ажурирању нема директну везу са њим. На срећу, прави deadlock се јавља прилично ретко.

Да видимо шта је тај “прави deadlock”. Да бисте то урадили, погледајте следећи дијаграм интеракције трансакција:

Имамо две конкурентне трансакције са опцијом чекања где се извршава UPDATE изјава. За разлику од случаја једноставног конфликта при ажурирању, овде можемо видети међузависни конфликт при ажурирању:

  • Трансакција #11 ажурира запис са кључем = 20, а трансакција #12 ажурира запис са кључем = 10;
  • Након тога, трансакција #11 ажурира запис са кључем = 10, а трансакција #12 ажурира запис са кључем = 20;

Као резултат, имамо ситуацију када свака трансакција мора да чека да се друга заврши и обе могу чекати бесконачно јер обе имају одређену опцију чекања. Наравно, сервер не може дозволити да се то догоди, па ће једна од трансакција бити приморана да се врати назад након временског ограничења одређеног у параметру DeadlockTimeout који је подразумевано постављен на вредност од 10 секунди.

Ову ситуацију можемо репродуковати уз помоћ два isql алата:

Након што се друга трансакција покрене, јавља се ситуација правог deadlock-а. Да би то сигурно открио, сервер покреће процедуру која се зове Deadlock scan - покреће се у интервалима једнаким DeadlockTimeout-у који је подразумевано једнак 10 секунди.

Имајте на уму да клијент (у овом случају isql) добија редовну поруку о конфликту при ажурирању, али је покренута за 10 секунди чак и ако је трансакција покренута са опцијом чекања.

Након што сервер открије међузависно закључавање две трансакције, такође ће повећати интерни бројач deadlock-а (можете га видети у fb_lock_print излазу).

Практична употреба Snapshot Table Stability

Сада када знамо како трансакције раде са конфликтним UPDATE изјавама, можемо се вратити на ниво изолације Snapshot Table Stability и пронаћи практичну употребу за њега.

Дакле, када је овај ниво изолације одређен, табела је закључана за писање, па чак и за читање.

Имајте на уму да ако табела у параметрима трансакције није експлицитно одређена, све табеле којима изјаве приступају у оквиру ове трансакције су закључане и то се дешава током првог приступа табели. Очигледно, ако се овај ниво изолације користи без опреза, лако ће довести до великог броја конфликата при ажурирању.

Reserving TableNN клаузула вам омогућава да одредите одређену табелу (или неколико табела) које ће бити закључане на почетку трансакције (такође је могуће одредити режим резервисања).

Ова одлична функција заједно са опцијом чекања омогућава вам да имплементирате веома ефикасан секвенцијални ред за промену одређене табеле.

У пракси, то изгледа овако - они клијенти који треба да направе ред за одређену табелу покрећу SNAPSHOT TABLE STABILITY трансакцију одређујући ову табелу, а затим покушавају да изврше операцију у оквиру ове трансакције и одмах је заврше.

На пример, желимо да направимо секвенцијално растући бројач у табели са јединим записом типа CREATE TABLE Table1(i1 integer not null), али из неког разлога не можемо користити генератор.

Псеудокод изгледа отприлике овако:

Code
set transaction snapshot table stability reserving TABLE1 for protected write

UPDATE Table1 Set i1 = i1+1;
SELECT i1 from Table1;
COMMIT;

Ако овај код покренемо не са нивоом изолације Snapshot Table Stability (Table1), већ са нижим нивоом изолације, могуће је да конкурентна UPDATE наредба интервенише између почетка трансакције и њене UPDATE наредбе. Као резултат, добићемо или изузетак при ажурирању одмах (nowait) или ће се наредба замрзнути до краја конкурентне (wait) или ће доћи до тајмаута (wait interval) - другим речима, конфликт ће се некако решити на нивоу наредбе.

Са нивоом изолације snapshot table stability, заштићени смо од тога јер је табела резервисана на почетку трансакције - она је или у потпуности наша или у потпуности није наша. Ако наведемо опцију wait за решавање конфликата, паралелне везе ће аутоматски формирати ред без обраде било каквих грешака.

Наравно, овај приступ се може применити само на кратке трансакције (као што је у нашем примеру).

У пракси, ниво изолације Snapshot table stability се користи за формирање редова и поновно израчунавање сложене логике у ексклузивном режиму (у релативно малим табелама или када нема других корисника).

Унутар мотора, Firebird користи ниво изолације Snapshot Table Stability за креирање индекса - тј. када извршите наредбу ALTER INDEX indexname ACTIVE;, Firebird ће у потпуности заузети табелу за коју се индекс гради.

Шта даље?

Овај чланак даје само увод у концепте Firebird трансакција. Да бисте потпуно разумели како трансакције функционишу у Firebird-у, неопходно је размотрити мултигенерацијску архитектуру (концепти верзија записа и сакупљања смећа), размотрити маркере трансакција (Oldest Interesting, Oldest Active, Oldest Snapshot, next) и друге ствари.

Чланак је заснован на материјалима семинара/радионице “All About Transactions”, који је први пут представљен 2013. године током семинара Firebird Tour, и на основу обуке IBSurgeon-а " Firebird Transaction in details".

Контакти

[email protected] Слободно нас контактирајте са било каквим питањима или сугестијама: [email protected]