Witam.

Napotkalem na problem, z ktorym borykam sie juz kolejny dzien - niestety odpowiedzi nadal nie znalazlem. Po krotce - choc nie wiem czy da sie tak bardzo ograniczyc winksmiley.jpg

W pewnym systemie rozpoczalem uzytkowanie tabel InnoDB - jak sie pewnie domyslacie - ze wzgledu na koneicznosc uzytkowania transakcji.

Konfiguracja to MySQL 4.0.21 (serwer) oraz klient w wersji 5.0.33 (na tej maszynie stoi drugi serwer MySQL - w wersji wlasnie 5.0.33).

Teraz wezmy jeden z problemow - problem licznikow. Mam kilka tabel, do ktorych laduje rekordy - musze recznie utrzymywac liczniki dla kazdej z tych tabel (kazdy rekord posiada kilka kolumn zawierajacych rozne formaty roznych licznikow).

Ale nas interesuje sama tabela licznikow. Jest to tabela InnoDB z powiedzmy 3 kolumnami:

Kod
table_liczniki

id, licznik_1, licznik_2


Teraz.. w systemie pracuje 200 uzytkownikow. Zdarzaja sie sytuacje, gdy to wielu z nich - praktycznie w tej samej chwili - stara sie dodac nowy rekord do tabel z danymi. Aby to zrobic skrypt PHP musi pobrac odpowiednie wartosci licznikow a nastepnie je zwiekszyc - skoro zostaly juz uzyte.. Bez zastosowania transakcji pojawil sie problem, iz ponizszy kod:

  1. UPDATE table_liczniki SET licznik_1=licznik_1+1 WHERE id=1;
  2. SELECT licznik_1 WHERE id=1;


wykonywany praktycznie rownolegle zwracal ta sama wartosc licznika. Oczywiscie problem bral sie stad, iz powyzszy kod wykonywany byl na tabelach MyIsam, ktore pobieraly rownolegle te same dane. Moglem zalatwic sprawe za pomoca table locka, lecz nie o to przeciez chodzi - skoro mozemy uzyc InnoDB oraz transakcji.. smile.gif

Teraz przejdzmy dalej. Jak sie domyslacie po napotkaniu powyzszego problemu zalozylem tabele InnoDB i rozpoczalem modyfikacje kodu dodajac obsluge transakcji. No i teoretycznie wszystko w porzadku. Na poczatku wytestowalem dzialalnosc za pomoca terminala - te same transakcje postaralem sie nalozyc na siebie. Wczesniej troche konfiguracji serwera:

  1. mysql> SELECT @@autocommit;
  2. +--------------+
  3. | @@autocommit |
  4. +--------------+
  5. | 1 |
  6. +--------------+
  7. 1 row IN SET (0.00 sec)


  1. mysql> SELECT @@global.tx_isolation;
  2. +-----------------------+
  3. | @@global.tx_isolation |
  4. +-----------------------+
  5. | REPEATABLE-READ |
  6. +-----------------------+
  7. 1 row IN SET (0.00 sec)
  8.  
  9. mysql> SELECT @@tx_isolation;
  10. +-----------------+
  11. | @@tx_isolation |
  12. +-----------------+
  13. | REPEATABLE-READ |
  14. +-----------------+
  15. 1 row IN SET (0.00 sec)


  1. mysql klient A> BEGIN;
  2. Query OK, 0 rows affected (0.00 sec)
  3.  
  4. mysql klient A> SELECT licznik_1 FROM table_liczniki WHERE id=1 FOR UPDATE;
  5. +--------------------+
  6. | licznik_1 |
  7. +--------------------+
  8. | 60000 |
  9. +--------------------+
  10. 1 row IN SET (0.00 sec)


W tej chwili odpalilem drugiego klienta i chcialem dokonac tego samego:

  1. mysql klient B> BEGIN;
  2. Query OK, 0 rows affected (0.00 sec)
  3.  
  4. mysql klient B> SELECT licznik_1 FROM table_liczniki WHERE id=1 FOR UPDATE;


I klient B "zawisl" - oczekujac na zakonczenie transakcji u klienta A. Gdy ta zostala zakonczona - za pomoca COMMIT - kleint B sie "odwiesil" pobral co mial do pobrania i wszyscy szczesliwi smile.gif

Teraz pozostala do sprawdzenia dzialalnosc systemu - czyli kodu PHP. W duzym uproszczeniu czesc kodu odpowiadajacego za owa transakcje wyglada tak:

  1. <?php
  2. $transaction = true;
  3.  
  4. $mysql = new mysql; // klasa do obslugi odbc
  5. $mysql->query('SET autocommit=0'); // wylaczamy autocommit..
  6. $mysql->query('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE'); // w zasadzie tylko dla pewnosci
  7.  
  8. $mysql->query('BEGIN;');
  9. $res = $mysql->query('SELECT licznik_1 FROM table_liczniki WHERE id=1;');
  10. if(!$res)
  11. {
  12. $transaction = false;
  13. }
  14. else
  15. {
  16. $row = mysql_fetch_assoc($res);
  17. }
  18. $res = $mysql->query('UPDATE table_liczniki SET licznik_1=licznik_1+1 WHERE id=1;');
  19. if(!$res)
  20. {
  21. $transaction = false;
  22. }
  23. if($transaction === true)
  24. {
  25. $mysql->query('COMMIT;');
  26. $licznik = $row['licznik_1'];
  27. }
  28. else
  29. {
  30. $mysql->query('ROLLBACK;');
  31. $licznik = false;
  32. }
  33. ?>


Aby sie przekonac sprawdzilem dzialalnosc tego kodu w przegladarce - system dziala poprawnie, liczniki ladnie sie nabijaja itd. Ale to metoda 'singlowa' - a nam chodzi o dzialalnosc multi - userowa. Czyli musimy ja zasymulowac. Znow uruchamiamy sesje terminala, laczymy sie z baza danych i jak ostatnio:

  1. mysql klient A> BEGIN;
  2. Query OK, 0 rows affected (0.00 sec)
  3.  
  4. mysql klient A> SELECT licznik_1 FROM table_liczniki WHERE id=1 FOR UPDATE;
  5. +--------------------+
  6. | licznik_1 |
  7. +--------------------+
  8. | 60000 |
  9. +--------------------+
  10. 1 row IN SET (0.00 sec)


I w tej chwili uruchamiamy skrypt w przegladarce.. no i skrypt wisi - strona sie ;wczytuje' - czyli wszystko w porzadku - PHP czeka na odpowiedz z serwera MySQL, a ten znow czeka na zakonczenie transakcji.. w chwili gdy dalem COMMIT w terminalu, strona w przegladarce sie "odwiesila" i wszystko zadzialalo poprawnie.

No to co tu duzo myslec - wystawiamy kod i testujemy na zywym organizmie. I juz w pierwszej minucie problemy - licznik powiela swoje stany, pomija niektore liczby.. slowem wariuje. Powiedzmy, ze mamy sytuacje taka po pobraniu kilku stanow - rekordy w tabelach zapisujacych stany licznikow wskazuja:

Kod
60001
60001
60003
60004
60004
60005
60007
60007


Itd... czyli cos sie.. cos nie dziala jak nalezy winksmiley.jpg Teraz w skrocie - gasilem serwer MysqL 4, pozostawiajac dzialajacy jedynie MySQL 5 - i na nim odpalalem powyzsze. Czyli wszelkie podejrzenia odnoszace sie do gryzienia sie serwerow odpadaja. Problem nie zniknal.
Probowalem dzialac na domyslnym ISOLATION LEVEL, czyli REPEATABLE-READ - zadnych efektow - tak samo jak SERIALIZABLE.
Probowalem nie ruszac autocommit pozostawiajac go domyslnie (czyli wlaczony) - w koncu BEGIN "wylacza" autocommit az do czasu COMMIT badz ROLLBACK. I nadal nic.

No to teraz logi systemowe. Tutaj klient wykonal transakcje i niby wszystko w porzadku:

Kod
07:29:59:::Modul_1:::SQL:mysql_query(BEGIN;)
07:29:59:::Modul_1:::SQL:mysql_query(SELECT `licznik_1` FROM `table_liczniki` WHERE id=1 FOR UPDATE;)
07:29:59:::Modul_1:::SQL:mysql_query(UPDATE `table_liczniki` SET `licznik_1`=`licznik_1`+'1' WHERE id=1;)
07:29:59:::Modul_1:::SQL:mysql_query(COMMIT;)
07:29:59:::Modul_1:::SQL:mysql_query(INSERT INTO tabela_z _rekordami (jakies_dane,licznik) VALUES('tralala','60000');)


Widac, ze w porzadku, gdyz UPDATE lecacy po COMMITowaniu transakcji podaje nam poprawna wartosc licznika.
A teraz jackpot - dwoch uzytkownikow rozpoczelo transakcje praktycznie w tej samej chwili. I oto efekt:

KLIENT 1:
Kod
07:34:12:::Modul_1:::SQL:mysql_query(BEGIN;)
07:34:12:::Modul_1:::SQL:mysql_query(SELECT `licznik_1` FROM `table_liczniki` WHERE id=1 FOR UPDATE;)
07:34:12:::Modul_1:::SQL:mysql_query(UPDATE `table_liczniki` SET `licznik_1`=`licznik_1`+'1' WHERE id=1;)
07:34:12:::Modul_1:::SQL:mysql_query(COMMIT;)
07:34:12:::Modul_1:::SQL:mysql_query(INSERT INTO tabela_z _rekordami (jakies_dane,licznik) VALUES('tralala','60001');)


KLIENT 2:
Kod
07:34:12:::Modul_1:::SQL:mysql_query(BEGIN;)
07:34:12:::Modul_1:::SQL:mysql_query(SELECT `licznik_1` FROM `table_liczniki` WHERE id=1 FOR UPDATE;)
07:34:12:::Modul_1:::SQL:mysql_query(UPDATE `table_liczniki` SET `licznik_1`=`licznik_1`+'1' WHERE id=1;)
07:34:12:::Modul_1:::SQL:mysql_query(COMMIT;)
07:34:12:::Modul_1:::SQL:mysql_query(INSERT INTO tabela_z _rekordami (jakies_dane,licznik) VALUES('tralala','60001');)
07:39:29:::Modul_1:::SQL:mysql_query(BEGIN;)
07:39:36:::Modul_1:::SQL:mysql_query(BEGIN;)
07:39:52:::Modul_1:::SQL:mysql_query(BEGIN;)

07:40:20:::Modul_1:::SQL:mysql_query(SELECT `licznik_1` FROM `table_liczniki` WHERE id=1 FOR UPDATE;): Lock wait timeout exceeded; try restarting transaction at address: jakis_adres
07:40:27:::Modul_1:::SQL:mysql_query(SELECT `licznik_1` FROM `table_liczniki` WHERE id=1 FOR UPDATE;): Lock wait timeout exceeded; try restarting transaction at address: jakis_adres
07:40:43:::Modul_1:::SQL:mysql_query(SELECT `licznik_1` FROM `table_liczniki` WHERE id=1 FOR UPDATE;): Lock wait timeout exceeded; try restarting transaction at address: jakis_adres

07:41:11:::Modul_1:::SQL:mysql_query(UPDATE `table_liczniki` SET `licznik_1`=`licznik_1`+'1' WHERE id=1;): Lock wait timeout exceeded; try re
starting transaction at address: jakis_adres
07:41:11:::Modul_1:::SQL:mysql_query(ROLLBACK;)

07:41:18:::Modul_1:::SQL:mysql_query(UPDATE `table_liczniki` SET `licznik_1`=`licznik_1`+'1' WHERE id=1;): Lock wait timeout exceeded; try re
starting transaction at address: jakis_adres
07:41:18:::Modul_1:::SQL:mysql_query(ROLLBACK;)

07:41:34:::Modul_1:::SQL:mysql_query(UPDATE `table_liczniki` SET `licznik_1`=`licznik_1`+'1' WHERE id=1;): Lock wait timeout exceeded; try re
starting transaction at address: jakis_adres
07:41:34:::Modul_1:::SQL:mysql_query(ROLLBACK;)


No i jakis kosmos. Po pierwsze jedna transakcja wrypala sie w druga... co jest bardzo dziwne. A po drugie - skad kolejne transakcje u klienta 2? (notabene nie udane)?

Jesli ktos ma jakis pomysl to zapraszam do przedstawienia smile.gif Glupioby bylo z takiego powodu tworzyc licznik przy uzyciu LOCKowania na poziomie rekordu w InnoDB. A poki co jedyne co mi pozostaje to chyba namierzyc powod, dla ktorych u klienta 2 na koncu pojawily sie 3 rozpoczecia transakcji