МВЦЦ-3. Стринг верзије

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

Наслов

Као што смо већ рекли, сваки ред може истовремено бити присутан у бази података у неколико верзија. Једна верзија се мора некако разликовати од друге.За ту сврху свака верзија има две ознаке које одређују „време“ деловања ове верзије (кмин и кмак). Под наводницима – јер се не користи време као такво, већ посебан бројач повећања. А овај бројач је број трансакције.

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

Када се креира ред, кмин се поставља на број трансакције која је издала наредбу ИНСЕРТ, а кмак се оставља празан.

Када се ред избрише, кмак вредност тренутне верзије је означена бројем трансакције која је извршила ДЕЛЕТЕ.

Када се ред измени наредбом УПДАТЕ, две операције се заправо изводе: ДЕЛЕТЕ и ИНСЕРТ. Тренутна верзија реда поставља кмак једнак броју трансакције која је извршила УПДАТЕ. Затим се креира нова верзија истог стринга; његова кмин вредност поклапа се са кмак вредношћу претходне верзије.

Поља кмин и кмак су укључена у заглавље верзије реда. Поред ових поља, заглавље садржи и друга, на пример:

  • инфомаска је низ битова који дефинишу својства ове верзије. Има их доста; Постепено ћемо размотрити главне.
  • цтид је веза ка следећој, новијој верзији исте линије. За најновију, најновију верзију стринга, цтид се односи на саму ову верзију. Број има облик (к,и), где је к број странице, и је број индекса у низу.
  • нулл битмап – Означава оне колоне дате верзије које садрже нулту вредност (НУЛЛ). НУЛЛ није једна од нормалних вредности типа података, тако да се атрибут мора чувати одвојено.

Као резултат тога, заглавље је прилично велико - најмање 23 бајта за сваку верзију линије, а обично више због НУЛЛ битмапе. Ако је табела „уска“ (то јест, садржи неколико колона), главни трошкови могу заузети више од корисних информација.

уметнути

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

За експерименте, хајде да направимо нову табелу са две колоне и индексом на једној од њих:

=> CREATE TABLE t(
  id serial,
  s text
);
=> CREATE INDEX ON t(s);

Хајде да убацимо један ред након покретања трансакције.

=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');

Ево нашег тренутног броја трансакције:

=> SELECT txid_current();
 txid_current 
--------------
         3664
(1 row)

Погледајмо садржај странице. Функција хеап_паге_итемс екстензије пагеинспецт вам омогућава да добијете информације о показивачима и верзијама редова:

=> SELECT * FROM heap_page_items(get_raw_page('t',0)) gx
-[ RECORD 1 ]-------------------
lp          | 1
lp_off      | 8160
lp_flags    | 1
lp_len      | 32
t_xmin      | 3664
t_xmax      | 0
t_field3    | 0
t_ctid      | (0,1)
t_infomask2 | 2
t_infomask  | 2050
t_hoff      | 24
t_bits      | 
t_oid       | 
t_data      | x0100000009464f4f

Имајте на уму да се реч хрпа у ПостгреСКЛ односи на табеле. Ово је још једна чудна употреба термина - позната је гомила структура података, који нема ништа заједничко са столом. Овде се реч користи у значењу „све је спојено“, за разлику од наређених индекса.

Функција приказује податке „као што јесу“, у формату који је тешко разумети. Да бисмо то схватили, оставићемо само део информација и дешифровати их:

=> SELECT '(0,'||lp||')' AS ctid,
       CASE lp_flags
         WHEN 0 THEN 'unused'
         WHEN 1 THEN 'normal'
         WHEN 2 THEN 'redirect to '||lp_off
         WHEN 3 THEN 'dead'
       END AS state,
       t_xmin as xmin,
       t_xmax as xmax,
       (t_infomask & 256) > 0  AS xmin_commited,
       (t_infomask & 512) > 0  AS xmin_aborted,
       (t_infomask & 1024) > 0 AS xmax_commited,
       (t_infomask & 2048) > 0 AS xmax_aborted,
       t_ctid
FROM heap_page_items(get_raw_page('t',0)) gx
-[ RECORD 1 ]-+-------
ctid          | (0,1)
state         | normal
xmin          | 3664
xmax          | 0
xmin_commited | f
xmin_aborted  | f
xmax_commited | f
xmax_aborted  | t
t_ctid        | (0,1)

Ево шта смо урадили:

  • Броју индекса је додана нула да би изгледао исто као т_цтид: (број странице, број индекса).
  • Дешифровано стање показивача лп_флагс. Овде је "нормално" - то значи да се показивач заправо односи на верзију стринга. Касније ћемо погледати друга значења.
  • Од свих битова информација, до сада су идентификована само два пара. Битови кмин_цоммиттед и кмин_абортед показују да ли је број трансакције кмин урезан (прекинут). Два слична бита се односе на број трансакције кмак.

Шта видимо? Када уметнете ред, на страници табеле ће се појавити индекс број 1, који указује на прву и једину верзију реда.

У верзији стринга, кмин поље је попуњено тренутним бројем трансакције. Трансакција је и даље активна, тако да и кмин_цоммиттед и кмин_абортед битови нису подешени.

Поље цтид верзије реда се односи на исти ред. То значи да новија верзија не постоји.

Поље кмак је попуњено лажним бројем 0 јер ова верзија реда није обрисана и актуелна је. Трансакције неће обраћати пажњу на овај број јер је постављен бит кмак_абортед.

Хајде да направимо још један корак ка побољшању читљивости додавањем битова информација бројевима трансакција. И хајде да креирамо функцију, пошто ће нам захтев требати више пута:

=> CREATE FUNCTION heap_page(relname text, pageno integer)
RETURNS TABLE(ctid tid, state text, xmin text, xmax text, t_ctid tid)
AS $$
SELECT (pageno,lp)::text::tid AS ctid,
       CASE lp_flags
         WHEN 0 THEN 'unused'
         WHEN 1 THEN 'normal'
         WHEN 2 THEN 'redirect to '||lp_off
         WHEN 3 THEN 'dead'
       END AS state,
       t_xmin || CASE
         WHEN (t_infomask & 256) > 0 THEN ' (c)'
         WHEN (t_infomask & 512) > 0 THEN ' (a)'
         ELSE ''
       END AS xmin,
       t_xmax || CASE
         WHEN (t_infomask & 1024) > 0 THEN ' (c)'
         WHEN (t_infomask & 2048) > 0 THEN ' (a)'
         ELSE ''
       END AS xmax,
       t_ctid
FROM heap_page_items(get_raw_page(relname,pageno))
ORDER BY lp;
$$ LANGUAGE SQL;

У овом облику, много је јасније шта се дешава у заглављу верзије реда:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3664 | 0 (a) | (0,1)
(1 row)

Сличне, али знатно мање детаљне, информације се могу добити из саме табеле, користећи псеудоколоне кмин и кмак:

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3664 |    0 |  1 | FOO
(1 row)

Фиксација

Ако је трансакција успешно завршена, потребно је да запамтите њен статус - имајте на уму да је посвећена. Да би се то урадило, користи се структура која се зове КСАЦТ (а пре верзије 10 звала се ЦЛОГ (дневник урезивања) и ово име се још увек може наћи на различитим местима).

КСАЦТ није табела системског каталога; ово су датотеке у ПГДАТА/пг_кацт директоријуму. Имају два бита за сваку трансакцију: извршену и прекинуту – баш као у заглављу верзије реда. Ове информације су подељене у неколико датотека искључиво ради погодности; вратићемо се на ово питање када размотримо замрзавање. И рад са овим датотекама се одвија страницу по страницу, као и са свим осталим.

Дакле, када је трансакција урезана у КСАЦТ-у, урезани бит се поставља за ову трансакцију. И ово је све што се дешава током урезивања (иако још не говоримо о дневнику претходног снимања).

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

  1. Да ли је кмин трансакција завршена? Ако није, онда креирана верзија стринга не би требало да буде видљива.
    Ова провера се врши гледањем друге структуре, која се налази у дељеној меморији инстанце и зове се ПроцАрраи. Садржи листу свих активних процеса, а за сваки је назначен број његове тренутне (активне) трансакције.
  2. Ако је завршено, како онда - обавезивањем или отказивањем? Ако се откаже, ни верзија реда не би требало да буде видљива.
    То је управо оно чему служи КСАЦТ. Али, иако се последње странице КСАЦТ-а чувају у баферима у РАМ-у, и даље је скупо сваки пут проверавати КСАЦТ. Стога, када се одреди статус трансакције, он се уписује у битове кмин_цоммиттед и кмин_абортед верзије стринга. Ако је један од ових битова постављен, тада се стање трансакције кмин сматра познатим и следећа трансакција неће морати да приступи КСАЦТ-у.

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

Лоша страна уштеде је у томе што након промена, свака трансакција (чак и она која обавља једноставно читање - СЕЛЕЦТ) може почети да мења странице података у кешу бафера.

Дакле, поправимо промену.

=> COMMIT;

Ништа се није променило на страници (али знамо да је статус трансакције већ забележен у КСАЦТ):

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3664 | 0 (a) | (0,1)
(1 row)

Сада ће трансакција која прва приступа страници морати да одреди статус кмин трансакције и да га упише у битове информација:

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | FOO
(1 row)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3664 (c) | 0 (a) | (0,1)
(1 row)

Брисање

Када се ред избрише, број тренутне трансакције брисања се уписује у поље кмак тренутне верзије, а бит кмак_абортед се брише.

Имајте на уму да подешена вредност кмак која одговара активној трансакцији делује као закључавање реда. Ако друга трансакција жели да ажурира или избрише овај ред, биће принуђена да сачека да се трансакција кмак заврши. Касније ћемо више причати о блокирању. За сада само напомињемо да је број закључавања редова неограничен. Они не заузимају простор у РАМ-у и перформансе система не пате од њиховог броја. Истина, „дуге“ трансакције имају и друге недостатке, али о томе касније.

Хајде да избришемо линију.

=> BEGIN;
=> DELETE FROM t;
=> SELECT txid_current();
 txid_current 
--------------
         3665
(1 row)

Видимо да је број трансакције уписан у кмак поље, али битови информација нису подешени:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax | t_ctid 
-------+--------+----------+------+--------
 (0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 row)

Поништење

Прекидање измена функционише слично као и урезивање, само у КСАЦТ-у је прекинути бит постављен за трансакцију. Поништавање је брзо као и преузимање. Иако се команда зове РОЛЛБАЦК, промене се не враћају уназад: све што је трансакција успела да промени на страницама података остаје непромењено.

=> ROLLBACK;
=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax | t_ctid 
-------+--------+----------+------+--------
 (0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 row)

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

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | FOO
(1 row)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   |   xmax   | t_ctid 
-------+--------+----------+----------+--------
 (0,1) | normal | 3664 (c) | 3665 (a) | (0,1)
(1 row)

Ажурирање

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

=> BEGIN;
=> UPDATE t SET s = 'BAR';
=> SELECT txid_current();
 txid_current 
--------------
         3666
(1 row)

Упит производи један ред (нова верзија):

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | BAR
(1 row)

Али на страници видимо обе верзије:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3664 (c) | 3666  | (0,2)
 (0,2) | normal | 3666     | 0 (a) | (0,2)
(2 rows)

Избрисана верзија је означена тренутним бројем трансакције у кмак пољу. Штавише, ова вредност је уписана преко старе, пошто је претходна трансакција отказана. И бит кмак_абортед се брише јер статус тренутне трансакције још није познат.

Прва верзија линије сада се односи на другу (т_цтид поље) као новију.

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

Баш као и код брисања, кмак вредност у првој верзији реда је индикација да је ред закључан.

Па, хајде да завршимо трансакцију.

=> COMMIT;

Индекси

До сада смо говорили само о страницама табеле. Шта се дешава унутар индекса?

Информације на страницама индекса увелико варирају у зависности од специфичног типа индекса. Чак и један тип индекса има различите типове страница. На пример, Б-стабло има страницу са метаподацима и „обичне“ странице.

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

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

Најважнија ствар је да у индексима било ког типа нема верзија редова. Па, или можемо претпоставити да је свака линија представљена тачно једном верзијом. Другим речима, у заглављу реда индекса нема кмин и кмак поља. Можемо претпоставити да везе из индекса воде до свих верзија табела редова - тако да можете да схватите коју верзију ће трансакција видети само гледањем у табелу. (Као и увек, ово није цела истина. У неким случајевима, мапа видљивости може да оптимизује процес, али ћемо то детаљније погледати касније.)

Истовремено, на страници индекса налазимо показиваче на обе верзије, и тренутну и стару:

=> SELECT itemoffset, ctid FROM bt_page_items('t_s_idx',1);
 itemoffset | ctid  
------------+-------
          1 | (0,2)
          2 | (0,1)
(2 rows)

Виртуелне трансакције

У пракси, ПостгреСКЛ користи оптимизације које му омогућавају да „чува“ бројеве трансакција.

Ако трансакција чита само податке, то нема утицаја на видљивост верзија редова. Стога, процес услуге прво издаје виртуелни кид трансакцији. Број се састоји од ИД-а процеса и редног броја.

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

Виртуелни бројеви се ни на који начин не узимају у обзир у снимцима података.

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

=> BEGIN;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                         
(1 row)

Ако трансакција почне да мења податке, добија се прави, јединствени број трансакције.

=> UPDATE accounts SET amount = amount - 1.00;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                     3667
(1 row)

=> COMMIT;

Угнежђене трансакције

Сачувајте поене

Дефинисано у СКЛ-у сачувати поене (савепоинт), који вам омогућавају да откажете део трансакције без потпуног прекидања. Али ово се не уклапа у горњи дијаграм, пошто трансакција има исти статус за све промене и физички се подаци не враћају назад.

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

Угнежђене трансакције имају свој број (већи од броја главне трансакције). Статус угнежђених трансакција се бележи на уобичајен начин у КСАЦТ-у, али коначни статус зависи од статуса главне трансакције: ако је поништена, онда се поништавају и све угнежђене трансакције.

Информације о угнежђењу трансакција се чувају у датотекама у ПГДАТА/пг_субтранс директоријуму. Датотекама се приступа преко бафера у заједничкој меморији инстанце, организованих на исти начин као и КСАЦТ баферима.

Не мешајте угнежђене трансакције са аутономним трансакцијама. Аутономне трансакције не зависе једна од друге ни на који начин, али угнежђене трансакције зависе. У обичном ПостгреСКЛ-у нема аутономних трансакција, и, можда, најбоље: оне су потребне веома, веома ретко, а њихово присуство у другим ДБМС-овима изазива злоупотребе, од којих онда сви трпе.

Хајде да обришемо табелу, започнемо трансакцију и убацимо ред:

=> TRUNCATE TABLE t;
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
=> SELECT txid_current();
 txid_current 
--------------
         3669
(1 row)

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
(1 row)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3669 | 0 (a) | (0,1)
(1 row)

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

=> SAVEPOINT sp;
=> INSERT INTO t(s) VALUES ('XYZ');
=> SELECT txid_current();
 txid_current 
--------------
         3669
(1 row)

Имајте на уму да функција ткид_цуррент() враћа главни број трансакције, а не угнежђени број трансакције.

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3670 |    0 |  3 | XYZ
(2 rows)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3669 | 0 (a) | (0,1)
 (0,2) | normal | 3670 | 0 (a) | (0,2)
(2 rows)

Вратимо се на тачку чувања и убацимо трећи ред.

=> ROLLBACK TO sp;
=> INSERT INTO t(s) VALUES ('BAR');
=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3671 |    0 |  4 | BAR
(2 rows)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3669     | 0 (a) | (0,1)
 (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normal | 3671     | 0 (a) | (0,3)
(3 rows)

На страници настављамо да видимо ред који је додан отказаном угнежђеном трансакцијом.

Поправљамо промене.

=> COMMIT;
=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3671 |    0 |  4 | BAR
(2 rows)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3669 (c) | 0 (a) | (0,1)
 (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normal | 3671 (c) | 0 (a) | (0,3)
(3 rows)

Сада можете јасно видети да свака угнежђена трансакција има свој статус.

Имајте на уму да се угнежђене трансакције не могу експлицитно користити у СКЛ-у, то јест, не можете започети нову трансакцију без довршавања тренутне. Овај механизам се имплицитно активира приликом коришћења тачака чувања, као и приликом руковања ПЛ/пгСКЛ изузецима и у низу других, егзотичнијих случајева.

=> BEGIN;
BEGIN
=> BEGIN;
WARNING:  there is already a transaction in progress
BEGIN
=> COMMIT;
COMMIT
=> COMMIT;
WARNING:  there is no transaction in progress
COMMIT

Грешке и атомичност операција

Шта се дешава ако дође до грешке током извођења операције? На пример, овако:

=> BEGIN;
=> SELECT * FROM t;
 id |  s  
----+-----
  2 | FOO
  4 | BAR
(2 rows)

=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR:  division by zero

Дошло је до грешке. Сада се трансакција сматра прекинутом и никакве операције у њој нису дозвољене:

=> SELECT * FROM t;
ERROR:  current transaction is aborted, commands ignored until end of transaction block

Чак и ако покушате да унесете промене, ПостгреСКЛ ће пријавити прекид:

=> COMMIT;
ROLLBACK

Зашто се трансакција не може наставити након неуспеха? Чињеница је да би грешка могла да настане на начин да бисмо добили приступ делу промена – нарушила би се атомичност чак и не трансакције, већ оператера. Као у нашем примеру, где је оператер успео да ажурира једну линију пре грешке:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3669 (c) | 3672  | (0,4)
 (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normal | 3671 (c) | 0 (a) | (0,3)
 (0,4) | normal | 3672     | 0 (a) | (0,4)
(4 rows)

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

=> set ON_ERROR_ROLLBACK on
=> BEGIN;
=> SELECT * FROM t;
 id |  s  
----+-----
  2 | FOO
  4 | BAR
(2 rows)

=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR:  division by zero

=> SELECT * FROM t;
 id |  s  
----+-----
  2 | FOO
  4 | BAR
(2 rows)

=> COMMIT;

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

Наставак.

Извор: ввв.хабр.цом

Додај коментар