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

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
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:
UPDATE table_liczniki SET licznik_1=licznik_1+1 WHERE id=1; 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..

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:
mysql> SELECT @@autocommit; +--------------+ | @@autocommit | +--------------+ | 1 | +--------------+ 1 row IN SET (0.00 sec)
mysql> SELECT @@global.tx_isolation; +-----------------------+ | @@global.tx_isolation | +-----------------------+ | REPEATABLE-READ | +-----------------------+ 1 row IN SET (0.00 sec) mysql> SELECT @@tx_isolation; +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row IN SET (0.00 sec)
mysql klient A> BEGIN; Query OK, 0 rows affected (0.00 sec) mysql klient A> SELECT licznik_1 FROM table_liczniki WHERE id=1 FOR UPDATE; +--------------------+ | licznik_1 | +--------------------+ | 60000 | +--------------------+ 1 row IN SET (0.00 sec)
W tej chwili odpalilem drugiego klienta i chcialem dokonac tego samego:
mysql klient B> BEGIN; Query OK, 0 rows affected (0.00 sec) 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

Teraz pozostala do sprawdzenia dzialalnosc systemu - czyli kodu PHP. W duzym uproszczeniu czesc kodu odpowiadajacego za owa transakcje wyglada tak:
<?php $transaction = true; $mysql->query('SET autocommit=0'); // wylaczamy autocommit.. $mysql->query('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE'); // w zasadzie tylko dla pewnosci $mysql->query('BEGIN;'); $res = $mysql->query('SELECT licznik_1 FROM table_liczniki WHERE id=1;'); if(!$res) { $transaction = false; } else { } $res = $mysql->query('UPDATE table_liczniki SET licznik_1=licznik_1+1 WHERE id=1;'); if(!$res) { $transaction = false; } if($transaction === true) { $mysql->query('COMMIT;'); $licznik = $row['licznik_1']; } else { $mysql->query('ROLLBACK;'); $licznik = false; } ?>
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:
mysql klient A> BEGIN; Query OK, 0 rows affected (0.00 sec) mysql klient A> SELECT licznik_1 FROM table_liczniki WHERE id=1 FOR UPDATE; +--------------------+ | licznik_1 | +--------------------+ | 60000 | +--------------------+ 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
60001
60003
60004
60004
60005
60007
60007
Itd... czyli cos sie.. cos nie dziala jak nalezy

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');)
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');)
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;)
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
