Transakcje w globalach InterSystems IRIS

Transakcje w globalach InterSystems IRISInterSystems IRIS DBMS obsługuje ciekawe struktury przechowywania danych - globale. Zasadniczo są to klucze wielopoziomowe z różnymi dodatkami w postaci transakcji, szybkimi funkcjami do poruszania się po drzewach danych, blokadami i własnym językiem ObjectScript.

Więcej o globalach przeczytasz w serii artykułów „Globale to miecze skarbów do przechowywania danych”:

Drzewa. Część 1
Drzewa. Część 2
Rzadkie tablice. Część 3

Zainteresowało mnie, jak transakcje są realizowane w globalach, jakie są tam funkcje. Przecież jest to zupełnie inna struktura przechowywania danych niż zwykłe tabele. Dużo niższy poziom.

Jak wiadomo z teorii relacyjnych baz danych, dobra implementacja transakcji musi spełniać określone wymagania ACID:

A - Atomowość (atomowość). Rejestrowane są wszystkie zmiany dokonane w transakcji lub żadne zmiany.

C – Spójność. Po zakończeniu transakcji stan logiczny bazy danych musi być wewnętrznie spójny. Pod wieloma względami wymóg ten dotyczy programisty, jednak w przypadku baz danych SQL dotyczy także kluczy obcych.

Ja – Izoluję się. Transakcje przebiegające równolegle nie powinny na siebie wpływać.

D - Trwały. Po pomyślnym zakończeniu transakcji problemy na niższym poziomie (na przykład awaria zasilania) nie powinny mieć wpływu na dane zmienione przez transakcję.

Globale to nierelacyjne struktury danych. Zostały zaprojektowane tak, aby działać bardzo szybko na bardzo ograniczonym sprzęcie. Przyjrzyjmy się realizacji transakcji w globalach przy użyciu oficjalny obraz okna dokowanego IRIS.

Do obsługi transakcji w IRIS wykorzystywane są komendy: ROZPOCZNIJ, TCOMMIT, TROLLACK.

1. Atomowość

Najprostszym sposobem sprawdzenia jest atomowość. Sprawdzamy z konsoli bazy danych.

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

Następnie stwierdzamy:

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

Otrzymujemy:

1 2 3

Wszystko w porządku. Utrzymuje się atomowość: wszystkie zmiany są rejestrowane.

Skomplikujmy zadanie, wprowadźmy błąd i zobaczmy, jak transakcja zostanie zapisana, częściowo lub wcale.

Sprawdźmy ponownie atomowość:

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

Następnie na siłę zatrzymamy kontener, wystrzelimy i zobaczymy.

docker kill my-iris

To polecenie jest prawie równoznaczne z wymuszonym zamknięciem, ponieważ wysyła sygnał SIGKILL w celu natychmiastowego zatrzymania procesu.

Może transakcja została częściowo zapisana?

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

- Nie, nie przetrwało.

Wypróbujmy polecenie wycofania:

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)

Nic też nie przetrwało.

2. Spójność

Ponieważ w bazach danych opartych na globalach klucze są również tworzone na globalach (przypomnę, że global to struktura niższego poziomu do przechowywania danych niż tabela relacyjna), aby spełnić wymóg spójności, należy uwzględnić zmianę klucza w tej samej transakcji co zmiana globalna.

Na przykład mamy globalną osobę, w której przechowujemy osobowości i używamy numeru TIN jako klucza.

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

Aby móc szybko wyszukiwać według nazwiska i imienia, utworzyliśmy klucz ^index.

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

Aby baza danych była spójna musimy dodać personę w następujący sposób:

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

W związku z tym przy usuwaniu musimy również skorzystać z transakcji:

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

Innymi słowy, spełnienie wymogu spójności spoczywa całkowicie na barkach programisty. Ale jeśli chodzi o globale, jest to normalne ze względu na ich niski poziom.

3. Izolacja

Tutaj zaczyna się dzicz. Wielu użytkowników jednocześnie pracuje na tej samej bazie danych, zmieniając te same dane.

Sytuacja jest porównywalna do sytuacji, gdy wielu użytkowników jednocześnie pracuje z tym samym repozytorium kodu i próbuje jednocześnie zatwierdzać zmiany w wielu plikach na raz.

Baza danych powinna to wszystko uporządkować w czasie rzeczywistym. Biorąc pod uwagę, że w poważnych firmach jest nawet specjalna osoba odpowiedzialna za kontrolę wersji (za łączenie oddziałów, rozwiązywanie konfliktów itp.), a baza danych musi to wszystko robić w czasie rzeczywistym, złożoność zadania i poprawność projekt bazy danych i kod, który ją obsługuje.

Baza danych nie jest w stanie zrozumieć znaczenia działań wykonywanych przez użytkowników, aby uniknąć konfliktów, jeśli pracują na tych samych danych. Może cofnąć tylko jedną transakcję, która powoduje konflikt z inną, lub wykonać je sekwencyjnie.

Kolejnym problemem jest to, że w trakcie realizacji transakcji (przed zatwierdzeniem) stan bazy danych może być niespójny, dlatego pożądane jest, aby inne transakcje nie miały dostępu do niespójnego stanu bazy danych, co osiąga się w relacyjnych bazach danych na wiele sposobów: tworzenie migawek, wierszy z wieloma wersjami itp.

Realizując transakcje równolegle ważne jest dla nas, aby nie kolidowały one ze sobą. To jest właściwość izolacji.

SQL definiuje 4 poziomy izolacji:

  • CZYTAJ NIEZGODNE
  • PRZECZYTAJ ZATWIERDZONE
  • POWTARZALNA CZYTANIE
  • SERIALIZOWANY

Przyjrzyjmy się każdemu poziomowi osobno. Koszty wdrożenia każdego poziomu rosną niemal wykładniczo.

CZYTAJ NIEZGODNE - to najniższy poziom izolacji, ale jednocześnie najszybszy. Transakcje mogą odczytywać zmiany wprowadzone przez siebie nawzajem.

PRZECZYTAJ ZATWIERDZONE to kolejny poziom izolacji, czyli kompromis. Transakcje nie mogą czytać między sobą zmian przed zatwierdzeniem, ale mogą czytać wszelkie zmiany wprowadzone po zatwierdzeniu.

Jeśli mamy długą transakcję T1, podczas której miały miejsce commity w transakcjach T2, T3...Tn, które pracowały na tych samych danych co T1, to żądając danych w T1 otrzymamy za każdym razem inny wynik. Zjawisko to nazywa się odczytem niepowtarzalnym.

POWTARZALNA CZYTANIE — na tym poziomie izolacji nie mamy do czynienia ze zjawiskiem jednorazowego odczytu, gdyż dla każdego żądania odczytu danych tworzona jest migawka danych wynikowych, a przy ponownym wykorzystaniu w tej samej transakcji dane z migawki Jest używane. Na tym poziomie izolacji można jednak odczytać dane fantomowe. Odnosi się to do odczytywania nowych wierszy, które zostały dodane przez transakcje zatwierdzone równolegle.

SERIALIZOWANY — najwyższy poziom izolacji. Charakteryzuje się tym, że dane wykorzystane w jakikolwiek sposób w transakcji (odczyt lub zmiana) stają się dostępne dla innych transakcji dopiero po zakończeniu pierwszej transakcji.

Najpierw sprawdźmy, czy istnieje izolacja operacji w transakcji od głównego wątku. Otwórzmy 2 okna terminala.

Kill ^t

Write ^t(1)
2

TSTART
Set ^t(1)=2

Nie ma izolacji. Jeden wątek widzi, co robi drugi, który otworzył transakcję.

Zobaczmy, czy transakcje różnych wątków widzą, co dzieje się w ich wnętrzu.

Otwórzmy 2 okna terminali i otwórzmy 2 transakcje równolegle.

kill ^t
TSTART
Write ^t(1)
3

TSTART
Set ^t(1)=3

Transakcje równoległe widzą swoje dane. Mamy więc najprostszy, ale i najszybszy poziom izolacji, CZYTAJ UNCOMMITED.

W zasadzie można się tego spodziewać po globalach, dla których wydajność zawsze była priorytetem.

A co, jeśli potrzebujemy wyższego poziomu izolacji w operacjach globalnych?

Tutaj musisz pomyśleć o tym, dlaczego poziomy izolacji są w ogóle potrzebne i jak działają.

Najwyższy poziom izolacji SERIALIZE sprawia, że ​​wynik transakcji realizowanych równolegle jest równoważny ich wykonaniu sekwencyjnemu, co gwarantuje brak kolizji.

Możemy to zrobić za pomocą inteligentnych blokad w języku ObjectScript, które mają wiele różnych zastosowań: za pomocą polecenia można wykonywać regularne, przyrostowe i wielokrotne blokowanie LOCK.

Niższe poziomy izolacji to kompromisy mające na celu zwiększenie szybkości bazy danych.

Zobaczmy, jak możemy osiągnąć różne poziomy izolacji za pomocą zamków.

Operator ten pozwala na przyjęcie nie tylko blokad wyłącznych potrzebnych do zmiany danych, ale tzw. blokad współdzielonych, które mogą przyjmować kilka wątków równolegle, gdy muszą odczytać dane, które nie powinny być zmieniane przez inne procesy w trakcie procesu odczytu.

Więcej informacji na temat metody blokowania dwufazowego w języku rosyjskim i angielskim:

Blokada dwufazowa
Blokowanie dwufazowe

Trudność polega na tym, że podczas transakcji stan bazy danych może być niespójny, ale te niespójne dane będą widoczne dla innych procesów. Jak tego uniknąć?

Za pomocą blokad stworzymy okna widoczności, w których stan bazy danych będzie spójny. A wszelki dostęp do takich okien widoczności uzgodnionego stanu będzie kontrolowany za pomocą zamków.

Udostępnione blokady tych samych danych nadają się do wielokrotnego użytku — może je przyjąć kilka procesów. Blokady te uniemożliwiają innym procesom zmianę danych, tj. służą do tworzenia okien spójnego stanu bazy danych.

Do zmiany danych stosowane są blokady wyłączne - tylko jeden proces może przyjąć taką blokadę. Ekskluzywny zamek może odebrać:

  1. Dowolny proces, jeśli dane są bezpłatne
  2. Tylko proces, który ma wspólną blokadę tych danych i jako pierwszy zażądał wyłącznej blokady.

Transakcje w globalach InterSystems IRIS

Im węższe okno widoczności, tym dłużej muszą na to czekać inne procesy, ale tym bardziej spójny może być stan bazy danych w jego obrębie.

READ_COMMITTED — istotą tego poziomu jest to, że widzimy tylko zatwierdzone dane z innych wątków. Jeżeli dane w innej transakcji nie zostały jeszcze zatwierdzone, wówczas widzimy ich starą wersję.

Dzięki temu możemy zrównoleglić pracę, zamiast czekać na zwolnienie blokady.

Bez specjalnych trików nie uda nam się zobaczyć starej wersji danych w IRIS, więc będziemy musieli zadowolić się blokadami.

W związku z tym będziemy musieli zastosować blokady współdzielone, aby umożliwić odczyt danych tylko w momentach spójności.

Załóżmy, że mamy bazę użytkowników, którzy przekazują sobie nawzajem pieniądze.

Moment przejścia z osoby 123 na osobę 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)

Momentowi zażądania kwoty pieniędzy od osoby 123 przed obciążeniem musi towarzyszyć ekskluzywna blokada (domyślnie):

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

A jeśli chcesz pokazać status konta na swoim koncie osobistym, możesz użyć wspólnej blokady lub w ogóle jej nie używać:

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

Jeśli jednak założymy, że operacje na bazie danych wykonywane są niemal natychmiastowo (przypomnę, że globale są strukturą znacznie niższego poziomu niż tabela relacyjna), to zapotrzebowanie na ten poziom maleje.

POWTARZALNA CZYTANIE — Ten poziom izolacji umożliwia wielokrotne odczyty danych, które można modyfikować w ramach jednoczesnych transakcji.

W związku z tym będziemy musieli założyć wspólną blokadę odczytu zmienianych przez nas danych i wyłączne blokady danych, które zmieniamy.

Na szczęście operator LOCK pozwala w jednym zestawieniu szczegółowo wyszczególnić wszystkie potrzebne blokady, a jest ich naprawdę sporo.

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

inne operacje (w tym momencie wątki równoległe próbują zmienić ^person(123, ilość), ale nie mogą)

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

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

Kiedy wyświetlasz listę blokad oddzielonych przecinkami, są one brane sekwencyjnie, ale jeśli to zrobisz:

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

następnie są one pobierane atomowo wszystkie na raz.

SERIALIZUJ — będziemy musieli ustawić blokady, aby docelowo wszystkie transakcje posiadające wspólne dane były wykonywane sekwencyjnie. W przypadku tego podejścia większość blokad powinna być ekskluzywna i uwzględniać najmniejsze obszary świata ze względu na wydajność.

Jeśli mówimy o obciążeniu środków w globalnej osobie ^, to akceptowalny jest dla niego tylko poziom izolacji SERIALIZE, ponieważ pieniądze muszą być wydawane ściśle sekwencyjnie, w przeciwnym razie możliwe jest wydanie tej samej kwoty kilka razy.

4. Trwałość

Przeprowadziłem testy z twardym docięciem pojemnika przy użyciu

docker kill my-iris

Baza dobrze je tolerowała. Nie stwierdzono żadnych problemów.

wniosek

Dla firm globalnych InterSystems IRIS zapewnia obsługę transakcji. Są naprawdę atomowe i niezawodne. Aby zapewnić spójność bazy danych opartej na globalach, wymagany jest wysiłek programisty i wykorzystanie transakcji, ponieważ nie ma ona wbudowanych skomplikowanych konstrukcji, takich jak klucze obce.

Poziom izolacji globali bez użycia blokad to READ UNCOMMITED, a przy stosowaniu blokad można to zapewnić aż do poziomu SERIALIZE.

Poprawność i szybkość transakcji na globalach w dużej mierze zależy od umiejętności programisty: im szerzej podczas odczytu używane są blokady współdzielone, tym wyższy poziom izolacji, a im węższe są stosowane blokady wyłączne, tym większa wydajność.

Źródło: www.habr.com

Dodaj komentarz