O nieblokującym INSERT - technikalia

Poniższy artykuł jest bardzo technicznym rozwinięciem ostrzeżenia o nie blokującym poleceniu INSERT.

Praktyczne podłoże problemu

Używaliśmy w kilku projektach middleware o nazwie RTR (Reliable Transaction Router, produkcji Compaq-a - dawniej Digitala). Jest to niezbyt znany produkt posiadający jedną unikalną cenną cechę - umożliwia odgrywanie transakcji na dwóch bazach danych. Działa to mniej więcej tak: mamy dwie maszyny, dwie (nic o sobie nie wiedzące) bazy danych, na każdej chodzą napisane przez nas serwery aplikacji. Jedna z maszyn jest 'główna' a druga zapasowa. Transakcja realizowana przez klienta trafia najpierw na maszynę główną, gdzie jest przetwarzana (wyniki wracają do klienta), jeśli zostanie zatwierdzona (zaCOMMITowana), zostaje odegrana (asynchronicznie) na maszynie zapasowej. Jeśli któraś z maszyn ulegnie awarii, druga dalej działa (zapasowa w razie potrzeby zostaje główną), a zmiany do odegrania są gromadzone (i zostają odegrane gdy można). W ten sposób mamy dwie w pełni zsynchronizowane bazy (maszyny na których się one znajdują mogą stać w odległości 100 km od siebie) i aplikację dostępną non-stop jeśli tylko którakolwiek z baz działa.

Przy implementacji powyższych mechanizmów pojawia się istotny problem z kolejnością - zamiana kolejności dwóch transakcji przy ich odgrywaniu na maszynie zapasowej może zmienić efekty ich realizacji. RTR nie może ich odgrywać 'po jednej' w tej samej kolejności w jakiej były wykonane na głównej maszynie - bo będzie za wolno. Dlatego stosuje sprytny algorytm określania, które dwie transakcje mogą być odegrane równocześnie, bo operują na nie związanych ze sobą danych. Uznaje, że jeśli dwie transakcje są równocześnie zatwierdzane (dokładniej, jeśli obie naraz są w momencie po ostatnim poleceniu a przed commitem), to znaczy, że nie są ze sobą skonfliktowane (bo inaczej jedna by drugą zablokowała w wcześniejszej fazie) - i można je odgrywać równocześnie (czyli między innymi można zamienić ich kolejność).

W tym kontekście 'nieblokujący INSERT' powoduje ogromne problemy. Transakcja realizująca INSERT i transakcja robiąca jakiś zbiorowy UPDATE się nie blokują i mogą być odegrane w odwrotnej kolejności. Budując aplikację trzeba stale o tym myśleć i wprowadzać dodatkowe zabezpieczenia wymuszające blokowanie transakcji.

Odrobina teorii

Jeszcze słówko z drugiej strony. Zgodnie z teorią baz danych tryb izolacji serializable oznacza, że transakcje mogą być wykonywane równocześnie ale ostateczny wynik odpowiada jakiemuś wykonaniu seryjnemu (tj. takiemu, gdy transakcje wykonujemy po kolei, zaczynając następną gdy poprzednia się całkowicie zakończyła). Popatrzmy na następującą sekwencję operacji (z lewej jedna sesja, z prawej druga, czas biegnie w dół, tablica TEST zawiera jedno pole id, nie ma indeksów i jest na początku pusta). W obu sesjach możemy wykonać

ALTER SESSION SET isolation_level=serializable;

- nic to nie zmienia.

Sesja1Sesja2
INSERT INTO test VALUES(1);
INSERT INTO test VALUES(2);
UPDATE test SET id = 3;
UPDATE test set id = 4;
COMMIT;
COMMIT;

Wynik - tablica test zawiera dwa rekordy: ('3') i ('4'). Przy żadnym wykonaniu seryjnym zdarzyć się to nie może (są tylko dwie możliwości, dostaniemy bądź dwie trójki bądź dwie czwórki). Nie wiem co Oracle rozumie przez serializable ale w każdym razie nie to, co to słowo oznacza. Z kilku innych baz jakie oglądałem żadna nie pozwala na takie wykonanie (blokuje się któryś z UPDATE i czeka na koniec konkurencyjnej transakcji).

Powyższy przykład jest dość naiwny (totalne update na tablicy bez klucza) ale bez trudu można go rozbudować do praktycznych zastosowań. Podobnie sprawa wygląda w różnych sytuacjach związanych z UPDATE kolumny nie indeksowanej w tablicy z kluczem.

Na koniec cytat z jednej z dyskusji wszczętej przez nas na którymś z anglojęzycznych forum:

Check out the discussion on this issue on the "comp.databases.oracle.server" newsgroup. All the messages in that thread can be found on "deja.com" at http://www.deja.com/%5bST_Tracker%3d0508%5d/thread/484313584.1. The opinion of the contributors to the thread appears to be that Oracle8 does not guarantee serializability (the requirement that the interleaved execution of transactions produce the same result as some serially executed sequence of these transactions) and is therefore not SQL92 compliant.

It appears that Oracle does in fact protect transactions from the three famous anomalies i.e. "dirty reads", "non-repeatable reads" and "phantoms". However, serializability theory requires "predicate locks" i.e. locks on the entire universe of rows that might satisfy the query "predicates" (selection criteria), not just locks on rows found in the database. Apparently, Oracle does not acquire such locks and hence cannot guarantee serializability.

The TPC standards apparently do not require the database engine to guarantee serializability, only protection from these three anomalies. However, to the best of my knowledge, the Federal Information Processing Standards (FIPS) require that the database engine guarantee serializability.

The question that remains unanswered is how serializability may be manually achieved when using Oracle. Researchers at the University of Masachusetts at Boston are attempting to answer this question. Their latest report may be found at http://www.cs.umb.edu/~isotest/feb99report.html.

In my assessment, database triggers and integrity constraints are not the answer because they act simply as extensions of your program code. I have a simple theory that you could implement serializability by logically partititioning your Oracle database and require that every transaction that writes to one of these partitions also update a database record that controls the affected partition. In a flight reservation system, you could require that before or after accepting a new reservation, the program update the "summary record" for the flight.

komentarze obsługiwane przez Disqus