Znane powiedzenie mówi są tylko dwie rzeczy trudne w informatyce – pierwsza to nazewnictwo, druga to inwalidacja cache. Autor tego powiedzenia najwyraźniej nie natrafił nigdy na problemy z synchronizacją procesów.
Wątki… Wszędzie wątki
Łatwo zapomnieć, że chociaż w PHP zazwyczaj nie piszemy asynchronicznego kodu, a w backendzie WordPress każda linia kodu jest wykonywana sekwencyjnie, to w praktyce serwer uruchamia i wykonuje jednocześnie wiele procesów PHP. Z tego powodu kod każdej wtyczki zawsze wykonywany jest współbieżnie (eng. concurrent). Zawsze musimy pamiętać o problemach, które pojawiają się w środowiskach wielowątkowych i rozproszonych.
Typowy problem WordPressowy gdzie wielowątkowość daje o sobie znać, to sytuacja, gdy na podstawie wartości zmiennej zapisanej w bazie danych, ustawiamy nową wartość tej zmiennej. Wbrew pozorom, takie sytuacje zdarzają się bardzo często.
Wąż w trawie
Załóżmy, że tak jak w WooCommerce istnieje post_type o nazwie product i w ramach jego meta danych przechowywana jest informacja, ile produktów znajduje się w magazynie. Gdy klient kupuje produkt, chcemy zmniejszyć liczbę produktów w magazynie o jeden. Jak to zrobić? Nic prostszego. Odczytujemy liczbę produktów w magazynie, zmniejszamy wartość o jeden i zapisujemy z powrotem.
Trywialne? Tak, do czasu gdy nie otrzymamy zgłoszenia od pierwszego wściekłego klienta, który sprzedał więcej produktów niż miał w magazynie. Następny klient zgłasza, że ma jeszcze produkty w magazynie, ale sklep twierdzi, że już się skończyły. Co się stało? Czy ktoś hackuje nam WordPressa i wstawia losowe liczby do bazy?
A może to dlatego, że nie używamy metod WooCommerce? Spróbujmy:
Teraz jest PRO. Idziemy spać pewni, że tym razem wszystko zrobiliśmy wzorcowo. Niestety rano przywitają nas kolejni smutni klienci. Co się stało? Jak mogliśmy zrobić krytyczny błąd w dwóch liniach kodu, który w oczywisty sposób jest poprawny?
Ale dlaczego to nie działa
Jak pewnie się domyślasz, problemem jest to, że jednocześnie wiele procesów PHP może modyfikować te same informacje. Wyobraźmy sobie, że ten sam produkt, jest w tym samym momencie kupowany przez dwóch różnych klientów. Oba procesy dzieją się równolegle. Proces pierwszy, pobiera liczbę produktów w magazynie, proces drugi milisekundę później również to robi, oba otrzymują tę samą wartość na przykład 9. Następnie oba procesy zmniejszają tę wartość o 1 i otrzymują 8 i oba zapisują liczbę 8 do bazy. Sprzedaliśmy dwa produkty, ale sklep myśli, że w magazynie nadal jest 8 produktów. Jeśli nikt tego nie zauważy, wkrótce zakupione zostaną produkty, których w magazynie nie ma.
Podczas gdy jeden proces PHP wykonywał spokojnie polecenia linia po linii, drugi proces wszedł w to samo miejsce w kodzie i wszystko pomieszał.
Czy taka sytuacja zdarzy się często, czy też jest to czysto akademicki problem? Niestety wbrew intuicji, ponieważ wątki mają tendencje do wspólnego czekania na operacje I/O, tego typu kod będzie często krzaczył się nawet w sklepach z niewielkim obciążeniem. To tylko kwestia czasu.
Jak rozwiązać ten problem?
Operacja atomowa
Często – i tak jest także i w tym przypadku, istnieje możliwość zastąpienia kilku operacji, za pomocą jednej operacji atomowej. Operacji atomowej to znaczy takiej, która nie daje się podzielić na mniejsze. Taka operacja jest albo wykonana, albo niewykonana i nigdy nie będzie wykonana “trochę”. Dzięki temu, że wykonamy pracę za pomocą niepodzielnej operacji, inne procesy PHP nie będą mogły wejść “pomiędzy” nasze polecenia. Za pomocą jednego polecenia jednocześnie pobierzemy, zmienimy i zapiszemy liczbę produktów w magazynie. Jak to zrobić?
Na pewno NIE tak:
Taki refactor niczego nie zmienia. Procesy PHP nadal będą wchodziły do funkcji decrease_stock i wykonywały jej kod linia po linii. Fakt, że funkcję możemy wywołać jednym poleceniem PHP nie sprawia, że operacja stała się atomowa.
Baza danych na ratunek
Możemy wykorzystać bazę danych i fakt, że pojedyncza operacja UPDATE/DELETE jest zawsze atomowa.
Teraz mamy kod, który zmniejszy liczbę produktów w magazynie za pomocą jednego niepodzielnego polecenia.
Zauważmy, że nie możemy użyć metody wpdb::update, ponieważ to polecenie nie umożliwia zmniejszenia wartości, a tylko na nadpisanie.
Co robić gdy zadanie nie daje się łatwo sprowadzić do pojedynczej operacji atomowej? W jaki sposób baza danych poradzi sobie z odczytaniem, zmianą i zapisaniem wartości w sposób atomowy? O tym podzielę się z wami w kolejnych wpisach!