Fəsil 07 · 9 dəq oxu

Transaction-lar

Transaction-lar

Ödəniş sistemində "A hesabından 100 manat çıx, B hesabına 100 manat əlavə et" iki ayrı yazmadır. Birincisi commit olunub, ikincisi zamanı proses düşübsə, pul sadəcə yoxa çıxıb. Eyni anda iki istifadəçi eyni son yeri rezerv edirsə, hər ikisi "boş yer var" oxuyub yazır və otel bir otağı iki nəfərə satır. Bu problemlər nə şəbəkəyə, nə də hardware-ə aiddir: onlar hətta tək maşında, tək database-də baş verir.

Transaction bu çətinlikləri sadələşdirmək üçün yaradılan abstraksiyadır: bir neçə oxuma və yazma məntiqi olaraq bir vahidə birləşdirilir, ya hamısı uğurlu olur, ya da hamısı ləğv edilir. Bu fəsil transaction-ın əslində nə vəd etdiyini, hansı anomaliyalardan qoruduğunu və hansılarından qorumadığını izah edir. Çünki "database transaction dəstəkləyir" cümləsi göründüyündən qat-qat az şey deyir.

ACID-in dürüst tərifi

ACID dörd hərfdir və hər database-də bir az fərqli məna daşıyır. Praktikada bu abreviatura marketinq terminə çevrilib, ona görə hər hərfi ayrıca açmaq lazımdır.

Atomicity "eyni anda" demək deyil, abortability deməkdir: transaction yarıda qalarsa, onun bütün yazmaları geri alınır. İstifadəçi üçün bunun mənası sadədir: xəta alanda təhlükəsiz şəkildə yenidən cəhd etmək olar, çünki yarımçıq nəticə qalmayıb.

Consistency burada ən zəif hərfdir. O, "data-nın həmişə application-ın invariant-larına uyğun olması"nı nəzərdə tutur: məsələn, bütün hesabların cəmi dəyişməməlidir. Amma bu invariant-ları database bilmir, onları application müəyyən edir. Database yalnız foreign key və unique kimi sadə yoxlaya bilər. Consistency application-ın xüsusiyyətidir, database-in yox.

Isolation paralel işləyən transaction-ların bir-birini görməməsidir. Klassik tərif serializability-dir: nəticə transaction-lar ardıcıl işləsəydi alınacaq nəticə ilə eynidir. Reallıqda database-lərin çoxu default olaraq daha zəif isolation verir və bu fəslin böyük hissəsi məhz bu zəifliklərə həsr olunub.

Durability commit olunmuş data-nın itməməsidir. Tək node-da bu, write-ahead log-un disk-ə fsync edilməsi deməkdir, replicated sistemdə isə bir neçə node-a kopyalanması. Lakin mütləq durability yoxdur: disk-lər korlanır, firmware bug-ları fsync-i yalan danışmağa məcbur edir, bütün replica-lar eyni datacenter-də yana bilər. Durability risk azaltmadır, zəmanət deyil.

ACID-in əksi kimi təklif edilən BASE (Basically Available, Soft state, ) isə daha da qeyri-müəyyəndir: praktikada sadəcə "ACID deyil" mənasını verir.

Tək obyekt və çox obyekt əməliyyatları

Hətta tək bir sətri yazanda atomicity və isolation lazımdır: 20 KB-lıq JSON sənədi yazarkən şəbəkə kəsilsə, yarım sənəd saxlanmamalıdır. Storage engine-lər bunu tək obyekt səviyyəsində log və lock ilə təmin edir. Bəzi sistemlər bundan bir az irəli gedir: increment və compare-and-set kimi atomik əməliyyatlar verir.

Lakin real tətbiqlər bir neçə obyekti birlikdə dəyişməlidir:

  • Foreign key-lər: order yazıldısa, order item-lər də yazılmalıdır.
  • Denormalised data: Fəsil 2-dəki document model-də eyni məlumat bir neçə yerdə təkrarlanır və hamısı birlikdə yenilənməlidir.
  • Secondary index-lər: Fəsil 3-dəki hər index ayrıca strukturdur; sətir dəyişib index dəyişməyibsə, sorğular ziddiyyətli nəticə verir.

Bu halların heç biri tək obyekt atomicity ilə həll olunmur. Çox obyektli transaction-lar olmayanda application özü yarımçıq vəziyyətləri idarə etməlidir və bu, kodun ən xətalı hissəsinə çevrilir.

Xəta və abort-u idarə etmək

Transaction-ın əsas vədi "uğursuz olanda yenidən cəhd et"dir, amma öz tələləri var:

  • Transaction əslində commit olunub, yalnız cavab şəbəkədə itib. əməliyyatı iki dəfə icra edir. Çarə əməliyyatlar və ya client tərəfindən verilən unikal transaction id-dir.
  • Xəta overload-dan yaranıbsa, dərhal yükü daha da artırır. və limiti lazımdır.
  • Deadlock və ya isolation konflikti kimi müvəqqəti xətaları etmək olar; pozuntusu kimi daimi xətaları yox.
  • Transaction-ın side effect-ləri varsa, məsələn e-poçt göndərir, abort e-poçtu geri çağırmır. Side effect-lər commit-dən sonra, ayrı mexanizmlə edilməlidir.

Zəif isolation səviyyələri

Serializable isolation baha başa gəlir, ona görə database-lər zəif səviyyələr təklif edir. Onlar bəzi concurrency anomaliyalarının qarşısını alır, bəzilərini isə yox. Hər səviyyəni konkret anomaliya ilə tanımaq lazımdır.

Read committed

İki zəmanət verir: dirty read yoxdur, yəni yalnız commit olunmuş data oxunur; dirty write yoxdur, yəni yalnız commit olunmuş data-nın üstünə yazılır. İkincisi sadə: hər sətir üçün yazma lock-u var və transaction commit olana qədər saxlanılır. Birincisi lock ilə həll edilmir, çünki uzun yazma transaction-ı bütün oxuyanları saxlayardı. Əvəzinə database hər sətrin köhnə commit olunmuş dəyərini saxlayır və oxuyanlara onu verir.

Read committed-in tutmadığı anomaliya read skew-dur: Alisa iki hesabının cəmini yoxlayır, hər hesabda 500 manat var. Hesab 1-i oxuyur: 500. Bu anda transfer transaction-ı hesab 1-dən hesab 2-yə 100 manat köçürüb commit edir. Alisa hesab 2-ni oxuyur: 600. Cəm 1100-dür, 100 manat havadan yaranıb. Hər oxuma commit olunmuş data-nı görüb, amma fərqli anlarda. Ekranı yeniləsə düzələr, lakin uzun sürən analitik sorğu və ya backup üçün bu "sonra düzələcək" deyil: backup daimi olaraq ziddiyyətli vəziyyəti saxlayır.

Snapshot isolation

Həll snapshot isolation-dır: hər transaction database-in başlanğıc anındakı consistent snapshot-ını görür. Sonradan nə commit olunsa da, transaction ona görünmür. Bu, uzun oxumalar, backup və analitika üçün idealdır.

Implementasiya MVCC, yəni multi-version concurrency control-dur. Hər transaction artan unikal txid alır. Sətrin hər versiyası iki sahə daşıyır: onu yaradan və onu silən (və ya yeni versiya ilə əvəz edən) transaction id-si. Update əslində silmə və yeni versiya əlavə etməkdir. Görünürlük qaydası belədir:

versiya görünür, əgər:
  created_by transaction snapshot anında commit olunmuşdu
  və (deleted_by boşdur
      və ya deleted_by transaction snapshot anında commit olunmamışdı)

Snapshot anında aktiv olan transaction-ların siyahısı yadda saxlanılır və onların yazdıqları hətta sonra commit olunsalar da görünmür. Index-lər üçün iki yanaşma var: index bütün versiyalara işarə edir və filtr görünürlüyə görə edilir; yaxud copy-on-write versiyası hər snapshot üçün ayrı kök yaradır.

Adlandırma qarışıqdır: PostgreSQL və MySQL bu səviyyəni "repeatable read" adlandırır, Oracle "serializable", bəzi sistemlər isə "snapshot isolation". SQL standartı anlayışı dəqiq təsvir etmədiyinə görə adlar sistemdən sistemə fərqlənir.

Lost update-in qarşısını almaq

Read committed və snapshot isolation əsasən oxuyanları qoruyur. Yazanlar arasında isə lost update adlı anomaliya var: iki transaction eyni dəyəri oxuyur, dəyişdirir və yazır. Counter-i artırmaq, JSON sənədində bir sahəni dəyişmək, iki istifadəçinin eyni wiki səhifəsini redaktə etməsi hamısı bu nümunədir. Həll yolları:

  • Atomik yazma əməliyyatları: UPDATE counters SET value = value + 1 WHERE key = 'foo'. Database sətri lock edib oxuyur və yazır. Mümkün olanda ən yaxşı seçimdir.
  • Explicit locking: SELECT ... FOR UPDATE sətri oxuyarkən lock edir, digərləri gözləyir. Unutmaq asandır və race condition yaranır.
  • Avtomatik aşkarlama: bəzi snapshot isolation implementasiyaları (PostgreSQL, Oracle, SQL Server) lost update-i aşkarlayıb transaction-ı abort edir. MySQL InnoDB bunu etmir.
  • Compare-and-set: UPDATE wiki SET content = 'yeni' WHERE id = 1 AND content = 'köhnə'. Dəyər dəyişibsə, update sıfır sətrə təsir edir və application yenidən cəhd edir. Snapshot-dan köhnə dəyəri oxuyan database-də işləməyə bilər.
  • Replicated kontekst: Fəsil 5-dəki multi-leader və leaderless sistemlərdə tək "ən son" versiya yoxdur, ona görə lock işləmir. Əvəzinə sibling-lər və merge funksiyaları işlədilir; LWW isə update-ləri sadəcə itirir.

Write skew və phantom-lar

Lost update-in ümumiləşmiş forması write skew-dur. Xəstəxanada növbə sistemində qayda var: hər növbədə ən azı bir həkim olmalıdır. Alisa və Bob eyni növbədədir, hər ikisi özünü xəstə hiss edir. Hər ikisinin transaction-ı "növbədə neçə həkim var?" sorğusu verir, cavab 2-dir, hər biri özünü silir və commit edir. Nəticədə növbədə heç kim qalmır. Heç bir sətir iki dəfə yazılmayıb, ona görə lost update aşkarlaması işə düşmür. Snapshot isolation bunu tutmur.

Eyni pattern başqa yerlərdə də var: iclas otağının eyni vaxta iki dəfə rezerv edilməsi, oyunda iki fiqurun eyni xanaya köçürülməsi, eyni username-in iki nəfərə verilməsi, iki dəfə pul çıxarıb balansı mənfiyə salmaq. Hamısında struktur eynidir:

  1. SELECT ilə şərt yoxlanılır: "otaq boşdur?", "username mövcuddur?"
  2. Nəticəyə görə qərar verilir.
  3. Yazma edilir və yazma 1-ci addımın nəticəsini dəyişir.

Bir transaction-ın yazmasının digərinin axtarış nəticəsini dəyişməsinə phantom deyilir. Həkim nümunəsində FOR UPDATE ilə mövcud sətirləri lock etmək olar, amma otaq rezervində lock ediləcək sətir hələ yoxdur: boş otaq cədvəldə sətir deyil. Çarələrdən biri konflikti materiallaşdırmaqdır: hər otaq və 15 dəqiqəlik interval üçün əvvəlcədən sətir yaradılır və rezerv transaction-ı həmin sətri lock edir. İşləyir, amma concurrency məsələsini data model-ə sızdırır. Daha təmiz həll serializable isolation-dır.

Serializability

Serializable isolation ən güclü səviyyədir: transaction-lar paralel icra olunsa belə, nəticə onların hansısa ardıcıl sırada icrasına bərabərdir. Üç fərqli implementasiya üsulu var.

Həqiqi ardıcıl icra

Ən sadə yol transaction-ları həqiqətən bir-bir, tək thread-də icra etməkdir. 2007-ci ilə qədər bu, qeyri-real sayılırdı; RAM ucuzlaşıb bütün dataset yaddaşa sığanda və transaction-larının qısa olduğu anlaşılanda mümkün oldu. Şərt: interaktiv transaction-lar olmamalıdır, çünki şəbəkə gözləməsi bütün sistemi dayandırar. Transaction bütünlüklə stored procedure kimi database-ə göndərilir. tək CPU nüvəsi ilə məhdudlaşır; daha çox lazımdırsa, Fəsil 6-dakı kimi partition-lara bölünür və hər partition öz thread-ində işləyir. Partition-ları aşan transaction-lar isə çox yavaşdır.

Two-phase locking

Onilliklər boyu əsas üsul 2PL olub. Qayda: oxumaq üçün shared lock, yazmaq üçün exclusive lock alınır; shared lock-lar bir-biri ilə uyğundur, exclusive heç nə ilə uyğun deyil; bütün lock-lar transaction sonuna qədər saxlanılır. Snapshot isolation-dan fərq budur: orada oxuyanlar yazanları, yazanlar oxuyanları bloklamır, 2PL-də isə bloklayır.

Phantom-lara qarşı predicate lock lazımdır: lock konkret sətirlərə deyil, "otaq 123, 12:00–13:00" kimi şərtə uyğun bütün mövcud və gələcək sətirlərə qoyulur. Bunu yoxlamaq bahadır, ona görə praktikada index-range lock işlədilir: şərt genişləndirilib index-in bir diapazonuna lock qoyulur. Daha çox obyekt lock olunur, amma yoxlama ucuzdur.

Qiyməti performans-dır: aşağıdır, gözlənilməzdir, çünki bir yavaş transaction hamını gözlədə bilər. Deadlock-lar tez-tez baş verir və database onlardan birini abort edib məcbur edir.

Serializable snapshot isolation

SSI 2008-ci ildə təklif edilib və PostgreSQL-in serializable səviyyəsinin əsasıdır. 2PL pessimistdir: konflikt ola bilərsə, gözləyir. SSI optimistdir: transaction snapshot isolation ilə işləyir, commit anında database onun oxuduqlarının köhnəlib-köhnəlmədiyini yoxlayır və köhnəlibsə abort edir. İki aşkarlama var:

  • Köhnə MVCC oxuması: transaction snapshot alanda başqa transaction-ın commit olunmamış yazması nəzərə alınmayıb. Həmin yazma commit olunubsa və bizim transaction da yazmaq istəyirsə, abort.
  • Əvvəlki oxumaya təsir edən yazma: index-range lock-a bənzər struktur hansı transaction-ların hansı diapazonu oxuduğunu qeyd edir; başqası həmin diapazona yazanda oxuyanlara xəbərdarlıq edilir və onlar commit-də abort olunur.

Üstünlüyü: oxuyanlar və yazanlar bir-birini bloklamır, read-only sorğular snapshot ilə sərbəst işləyir, proqnozlaşdırılandır. Zəif tərəfi: konflikt çox olanda abort nisbəti artır, ona görə transaction-lar qısa olmalıdır.

Səviyyələrin xülasəsi

Isolation səviyyəsiDirty readRead skewLost updateWrite skew / phantom
Read uncommittedmümkündürmümkündürmümkündürmümkündür
Read committedyoxdurmümkündürmümkündürmümkündür
Snapshot isolationyoxduryoxdursistemdən asılımümkündür
Serializable (2PL, SSI, serial)yoxduryoxduryoxduryoxdur

Fəsil 8 və 9-da görəcəyik ki, bu anlayışlar distributed mühitdə daha da mürəkkəbləşir: tək node-dakı lock distributed lock-a, tək commit isə atomic commit protokoluna çevrilir.

Tapşırıqlar

  1. İstifadə etdiyiniz database-in default isolation səviyyəsini tapın və sənədlərdəki adının bu fəsildəki hansı səviyyəyə uyğun gəldiyini müəyyən edin. İki paralel sessiya açıb read skew və lost update anomaliyalarını təkrarlamağa çalışın; hansı baş verir, hansı yox?
  2. Onlayn kursda "hər qrupda maksimum 30 tələbə" qaydası var. Qeydiyyat transaction-ının write skew-a məruz qaldığını göstərən ssenari yazın. Sonra üç həll təklif edin: materiallaşdırılmış konflikt, FOR UPDATE, serializable isolation. Hər birinin təsirini müzakirə edin.
  3. Ödəniş API-niz transaction uğursuz olanda edir. Hansı xətaları etmək olar, hansılarını yox? "Cavab şəbəkədə itdi" halı üçün mexanizmini pseudo-code ilə dizayn edin və onun özünün hansı isolation səviyyəsinə ehtiyacı olduğunu izah edin.