Kiedy złe rzeczy dzieją się dobrym danym, czyli podstawy transakcji w MySQL 4.1 emil@bronikowski.com Świat współczesny w którym oprogramowanie komputerowe zarządza większością aspektów naszego życia nauczył nas jednego. Oprogramowanie się psuje. Kiedy oprogramowanie się psuje, ludzie płaczą. Różni ludzie: klienci, których książeczka oszczędnościowa wykazuje stratę szesnastu milionów złotych, programiści, którzy muszą coś z tym zrobić oraz obsługujący te programy, bo klienci starają się ich udusić przez te małe otwory w okienkach a programiści nic nie robią, żeby to naprawić. Podsumowując: zepsuty program to straszny problem. Problem tym większy im ważniejsze dane ucierpiały. Gro danych spędza swoje życie w bazach danych. Różne bazy danych zapewniają różną funkcjonalność, ale mimo to do pewnego stopnia działają podobnie, w ten sam sposób w jaki Maluch i Ford działają podobnie -- jeden z nich ma silnik o parametrach kosiarki do trawy, ale oba są samochodami. W tym krótkim artykule chciałbym zająć się konceptem transakcji w serwerach baz danych MySQL. MySQL przez długi czas odstawał w funkcjonalności. Mimo to stał się bazą bardzo popularną i po pewnym czasie, dzięki masie krytycznej użytkowników, rozpoczął drogę ku nowym możliwością. Z pojawieniem się wersji 4.x serwer ten dorobił się silnika składowania danych wspierających transakcje. Co to jest ta transakcja i jaki ma związek ze wstępem? Ogólnie mówiąc transakcja to sposób w który zabezpieczamy nasze dane przez problemami. Bardzo wygodny sposób na odkręcenie tego, cośmy zepsuli. Transakcja to nic innego jak powiedzenie bazie -- Słuchaj, jest taka sprawa. Od tej pory zwracaj uwagę na wszystko co Ci będę mówił, rób to, ale tak, żeby się dało natychmiast odkręcić. Jak Ci powiem, że jest dobrze, zrób to i zapomnij. Wyobraźmy sobie, że projektujemy system w którym będziemy składować długi. Stwórzmy dwie tablice jako przykład takiego systemu: (Oczywiście, nie jest to optymalny schemat, ale nie będziemy teraz kombinować) CREATE TABLE `test`.`people` ( `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL, PRIMARY KEY(`id`) ) ENGINE = InnoDB COMMENT = 'Lista ludzi';
CREATE TABLE `test`.`cash` ( `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `fpersonid` INT(11) UNSIGNED NOT NULL, `spersonid` INT(11) UNSIGNED NOT NULL, `cash` FLOAT UNSIGNED NOT NULL, `operationtype` ENUM('LOAN','PAYBACK') NOT NULL DEFAULT 'LOAN', `performedat` DATETIME NOT NULL, PRIMARY KEY(`id`) ) ENGINE = InnoDB COMMENT = 'tu lezy kasa'; Proszę zwrócić uwagę na pogrubiony parametr ENGINE. InnoDB jest właśnie tym silnikiem, który potrafi dokonywać transakcji. Stwórzmy więc trochę danych na których można pokazać jak to wszystko działa. Na początek dodajmy kilka osób: mysql> INSERT INTO people(name) VALUES ('opi'),('shot'),('malina'); Query OK, 3 rows affected (0.08 sec) OK, teraz użytkownik opi (ID 1) będzie winien użytkownikowi Shot (ID 2) trochę pieniędzy. mysql> INSERT INTO cash(fpersonid, spersonid, cash, performedat) VALUES (2,1,600.00,NOW()) ; Query OK, 1 row affected (0.08 sec) Świetnie. Mamy teraz jakieś dane. To mogą być bardzo ważne dane dla użytkowników, szczególnie dla użytkownika o ID 2. Załóżmy teraz, że użytkownik ID 2 oddał część pieniędzy. mysql> INSERT INTO cash(fpersonid, spersonid, cash, performedat,operationtype) VALUES (1,2,2000.00,NOW(),'PAYBACK') ; Query OK, 1 row affected (0.24 sec) No ładnie. System się pomylił i oddał trochę za dużo. Operator próbuje więc dodać poprawny wpis... mysql> INSERT INTO cash(fpersonid, spersonid, cash, performedat,operationtype) VALUES (1,2,200.00,NOW(),'PAYBACK') ; Query OK, 1 row affected (0.24 sec)...no i skasować błędny. Niestety, znów się myli i zamiast jednego wpisu, kasuje wszystkie. mysql> DELETE FROM cash; Query OK, 3 rows affected (0.22 sec)
Od tej pory w firmie zaczyna się awantura. Kto to teraz ma naprawić? Kto będzie bez premii? Czy użytkownik ID 2 ma szansę odzyskać swoje pieniądze? Czy ID1 ujdzie to na sucho? Dlaczego nie robimy kopi bezpieczeństwa częściej. Każdy kto miał w swojej karierze okazję zepsucia czegoś większego zna pewnie takie sytuacje. Wyobraźmy sobie, że sytuacja jest już rozwiązana, kopia bezpieczeństwa się znalazła i starczył jeden nieodpłatny weekend pracy, żeby wszystko odkręcić. Niezbyt zadowoleni ludzie pytają więc programistów czy nie mogliby dać im szansy wycofać się z błędnych decyzji. Pada decyzja, żeby wszystkie ważne decyzje finansowe przeprowadzać podczas transakcji. Aby rozpocząć transakcje należy poinformować o tym bazę danych. Robi się to jednym poleceniem: mysql> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) Od tej pory system zapamięta wszystkie zmiany które wprowadzimy i da nam szansę na wycofanie się z nich rakiem (ROLLBACK) i potwierdzenie (COMMIT). Zobaczmy jak to działa w naszym przykładzie. Wiemy, że użytkownik ID3 nazywa się inaczej a obecna wartość pola name to zawołanie bojowe 1 -- chcemy zmienić więc dane tak, żeby odzwierciedlały one rzeczywistość. mysql> UPDATE people SET name = 'Marta' WHERE id = 2; Query OK, 1 row affected (0.41 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> SELECT name FROM people; name opi Marta Malina No i ładnie. Nie dość, że wcześniej pozbawiłem użytkownika ID 2 pieniędzy to teraz jeszcze ma uszczerbek na męskości. Na szczęście baza opiekowała się danymi bo byliśmy w trakcie transakcji, możemy więc spróbować naprawić nasz błąd. mysql> ROLLBACK; Query OK, 0 rows affected (0.04 sec) mysql> SELECT name FROM people; name opi Shot Malina Jak widzimy wszystkie zmiany zostały cofnięte. Jesteśmy uratowani. Tym razem zamiast manualnego odtwarzania danych nasz problem rozwiązał mechanizm transakcji. 1 Facet od PO mówił tak na pseudonimy
Ponieważ polityka historyczna jest teraz na topie, przytoczmy jeszcze jeden przykład. mysql> INSERT INTO people(name) VALUE ('Lech W.'); mysql> START TRANSACTION; (1,4,100000000.00,NOW()); (2,4,100000000.00,NOW()); (3,4,100000000.00,NOW()); Użytkownik ID4 jest winny innym użytkownikom po sto milionów (przed denominacją). Ponieważ wszystko jest OK, mówimy bazie, żeby dane w tej transakcji przyjęła za właściwe. mysql> COMMIT; Słowo o hermetyzacji w transakcji. Kiedy rozpoczynamy transakcję i zmieniamy dane podczas jej trwania, wszystkie zmiany są niewidoczne dla innych połączeń z bazą. Póki nie wywołaliście COMMIT dla poprzedniego przykładu inni użytkownicy systemu widzieli użytkownika ID4, Lecha W. ale już wszystkie operacje finansowe nie były częścią ich wiedzy. Jeśli projektujecie aplikację która jest używana przez kilka osób jednocześnie to warto o tym pamiętać. Jak więc widzicie transakcje są bardzo przydatnym mechanizmem w bazach danych. Chronią Was i zapewniają metodę wycofania się z różnego rodzaju błędów. W prawdziwym świecie nie używa się oczywiście ich tak, jak tu pokazałem. Wystarczy jednak pomyśleć o systemie, który rejestruje użytkowników. Podczas rejestracji, prócz danych użytkownika, tworzona jest także grupa i dołączana fotka. Co by się stało gdybyśmy nie dodawali takiego użytkownika podczas transakcji? Użytkownik traci połączenie z bazą np. w wyniku awarii sieci, zostawiając po sobie konto, ale takie, które nie ma dopisanych danych o grupie ani zdjęcia, gdyż pierwsze zapytanie się wykonało a inne już nie. W transakcji utrata połączenia może się równać z wywołaniem ROLLBACK i odkręceniem wszystkiego oraz umożliwieniem ponownej próby dla nieszczęśliwego użytkownika. Na koniec kilka luźnych uwag w temacie projektowania systemów i używania transakcji. InnoDB jest wolniejsze od standardowego silnika baz danych dostępnego w MySQL Nie wszystkie firmy utrzymujące strony dają możliwość tworzenia tablic typu InnoDB. Podyktowane jest to trochę mniejszymi możliwościami administracyjnymi (brak tak łatwej kontroli rozmiaru bazy) i większymi wymaganiami jeżeli chodzi o zasoby
Nie wszystko wymaga transakcji. To, że możesz coś zrobić, nie znaczy, że musisz. Jeżeli stracisz jedną linijkę logów w wyniku awarii to świat się nie zawali. Strata jednej wpłaty w systemie finansowym może być znacząca. Pamiętaj o hermetyzacji. ALTER/CREATE/DROP nie podlegają dyrektywie ROLLBACK. ALTER/CREATE/LOCK/UNLOCK powodują automatyczne wywołanie COMMIT