Przejdź do treści
DedicatedPHP Kontakt

Blokady rozproszone z Redis w PHP: kiedy pomagają, a kiedy nie

Dowiedz się, kiedy blokada rozproszona z Redis pomaga w PHP, jak bezpiecznie ją uzyskać i zwolnić oraz jakie gwarancje powinna zapewniać również baza danych.

Schemat koncepcyjny wielu workerów PHP koordynujących pracę za pomocą tymczasowej blokady w Redis i bazy danych

Gdy kilka workerów PHP może jednocześnie wykonywać zadanie dotyczące tego samego zasobu, blokada może ograniczyć powielanie pracy. Często rozważa się ją przy generowaniu raportu, przetwarzaniu zamówienia lub aktualizowaniu współdzielonego zasobu. Samo użycie Redis nie zapewnia jednak spójności operacji: proces może zostać wstrzymany, blokada może wygasnąć, a kolejny worker może rozpocząć pracę, zanim pierwszy ją zakończy.

Ważne jest nie tylko to, jak utworzyć klucz, lecz także to, jaką właściwość należy chronić i który system może ją egzekwować. W wielu przypadkach bardziej bezpośrednią gwarancję zapewnia ograniczenie unikalności albo transakcja w bazie danych. Blokady rozproszone z Redis w PHP są przydatne, gdy koordynują procesy, których nie da się obsłużyć pojedynczą atomową operacją w bazie danych, pod warunkiem że projektuje się je jako tymczasową ochronę, a nie ostateczne źródło prawdy.

Co rozwiązuje blokada rozproszona, a czego nie gwarantuje

Co rozwiązuje blokada rozproszona, a czego nie gwarantuje — guía visual de DedicatedPHP

Blokada rozproszona pozwala kilku procesom, potencjalnie działającym na różnych serwerach, rywalizować o tymczasową wyłączność. Jeśli worker uzyska blokadę, pozostałe powinny powstrzymać się od wejścia do sekcji krytycznej, dopóki blokada pozostaje ważna. Przydaje się to na przykład do zapobiegania jednoczesnemu uruchomieniu kosztownego zadania przez dwa procesy albo do koordynowania pracy wpływającej na usługi zewnętrzne.

Sama blokada nie gwarantuje, że nastąpi tylko jeden zapis, że proces będący jej właścicielem nadal działa ani że Redis i baza danych utrzymają spójny stan. Jest tymczasowym przydziałem, podobnym do lease. Jeśli worker zatrzyma się na dłużej niż czas trwania blokady, Redis może pozwolić innemu procesowi ją uzyskać, mimo że pierwszy worker będzie nadal pracował po wznowieniu działania.

Dlatego aplikacja nie powinna polegać wyłącznie na kluczu Redis przy ochronie reguły biznesowej. Operacja końcowa musi mieć własne zabezpieczenia: walidację, ograniczenia integralności, kontrolę wersji lub idempotencję — zależnie od przypadku.

Kiedy użyć Redis, a kiedy wybrać bazę danych

Zanim dodasz blokadę, określ warunek, który chcesz zachować. Jeśli chodzi o uniemożliwienie utworzenia dwóch rekordów tej samej encji z tym samym identyfikatorem, ograniczenie unikalności w bazie danych wyraża tę regułę i egzekwuje ją w miejscu zapisu danych. Nie zależy od tego, czy wszystkie workery przestrzegają zewnętrznego protokołu.

Jeśli kilka zmian musi zostać zatwierdzonych razem, może wystarczyć transakcja. Aby uniknąć nadpisania równoległej aktualizacji, można zastosować kontrolę optymistyczną: odczytać wersję, zaktualizować dane tylko wtedy, gdy wersja się nie zmieniła, i jawnie obsłużyć konflikt. W przypadku operacji na wierszach lub zbiorach danych właściwsza może być pesymistyczna blokada w samej bazie danych, choć jej zakres i czas trwania należy ograniczyć.

Redis może być dobrym wyborem, gdy blokada koordynuje pracę między procesami i nie da się jej rozwiązać pojedynczą operacją w bazie danych albo gdy trzeba zapobiec jednoczesnemu uruchamianiu powtarzalnego zadania przez wielu workerów. Oceń również, czy uzasadnione są koszty oczekiwania, ponawiania prób i obsługi Redis. Jeśli problem można rozwiązać za pomocą klucza unikalnego i mniejszej liczby komponentów, zwykle lepiej wybrać to rozwiązanie.

Atomowe uzyskanie blokady, pojedynczy właściciel i wygaśnięcie

Uzyskanie blokady powinno być pojedynczą atomową operacją: utworzeniem klucza tylko wtedy, gdy jeszcze nie istnieje, i przypisaniem mu czasu wygaśnięcia w ramach tej samej operacji. W Redis typowy wzorzec wykorzystuje SET z opcjami odpowiadającymi warunkowi „tylko jeśli nie istnieje” oraz czasowi życia. Oddzielne utworzenie klucza i przypisanie czasu wygaśnięcia powoduje ryzyko wyścigu: proces może ulec awarii pomiędzy tymi operacjami i pozostawić blokadę bez terminu wygaśnięcia.

Wartością klucza powinien być unikalny token wygenerowany dla danej próby, a nie stała wartość współdzielona przez wszystkie workery. Token pozwala zidentyfikować właściciela i zapobiega przypadkowemu usunięciu przez jeden proces blokady, którą później uzyskał inny. Klient PHP powinien uznać blokadę za uzyskaną wyłącznie wtedy, gdy Redis potwierdzi powodzenie operacji warunkowej.

// Pseudokod schematyczny; dostosuj wywołanie do używanej biblioteki Redis.
$token = wygenerujUnikalnyToken();
$uzyskano = redisUstawJesliNieIstniejeZWygasnieciem($klucz, $token, $czasTrwania);

if (!$uzyskano) {
    // Nie wchodź do sekcji krytycznej.
}

Czas wygaśnięcia powinien przekraczać przewidywany czas pracy, ale nie być tak długi, by awaria niepotrzebnie zmuszała inne procesy do oczekiwania. Nie ma jednej uniwersalnej wartości: mierz rzeczywisty czas trwania, uwzględniaj jego zmienność i określ, co robić z zadaniami zbliżającymi się do limitu. Odnowienie lease również wymaga sprawdzenia, czy token nadal należy do właściciela; bezwarunkowe odnowienie mogłoby przedłużyć blokadę innego workera.

Bezpieczne zwalnianie i ryzyko wygaśnięcia

Po zakończeniu pracy nie usuwaj klucza zwykłą operacją kasowania. Jeśli pierwotna blokada wygasła i inny proces uzyskał nową, pierwszy worker mógłby usunąć klucz należący do drugiego. Zwolnienie musi porównać zapisaną wartość z tokenem właściciela i usunąć klucz tylko wtedy, gdy wartości są zgodne — jako operację atomową. Można to zaimplementować za pomocą skryptu uruchamianego w Redis albo równoważnej funkcji, która porównuje i usuwa klucz bez przeplatania operacji.

Nawet przy prawidłowym uzyskiwaniu i zwalnianiu blokady długa pauza procesu może doprowadzić do równoległych zapisów. Worker A uzyskuje blokadę, zostaje wstrzymany i jego lease wygasa; worker B uzyskuje blokadę i zapisuje dane; A wznawia pracę i również zapisuje dane. Jeszcze gorzej, awaria połączenia lub partycja sieciowa może sprawić, że klient nie będzie wiedział, czy operacja uzyskania blokady została wykonana.

Jeśli spóźniony zapis może spowodować szkody, chroniony zasób musi odrzucać go niezależnie od blokady. Jedną ze strategii jest użycie rosnącego tokenu ogrodzenia (fencing token) lub wersji i wymaganie, by system odbierający zapis odrzucał starsze wartości. Licznik musi mieć wymagane w danym przypadku właściwości dotyczące kolejności i trwałości; nie należy automatycznie zakładać, że licznik w Redis zapewnia taką gwarancję bez uwzględnienia restartów lub failover. Inną możliwością jest sprawdzenie wersji w bazie danych w ramach samej operacji zapisu.

Sekcja krytyczna, rywalizacja o blokadę i błędy jej uzyskania

W blokadzie umieszczaj tylko tę pracę, która rzeczywiście wymaga wyłączności. Unikaj obejmowania nią powolnych wywołań usług zewnętrznych, oczekiwania na użytkownika lub niezależnego przetwarzania. Im dłużej trwa sekcja krytyczna, tym większa rywalizacja o blokadę i prawdopodobieństwo przekroczenia czasu jej wygaśnięcia. Jeśli pracę da się podzielić, oddziel krótką koordynację od zadań, które nie zależą od współdzielonego stanu.

Jeśli nie uda się uzyskać blokady, nie należy ponawiać próby bez końca ani zajmować workerów PHP długim oczekiwaniem. Określ politykę odpowiednią do wagi zadania: zrezygnuj i zarejestruj konflikt, ponów próbę po ograniczonym czasie oczekiwania albo umieść zadanie w kolejce, aby przetworzyć je później. Ponowienie musi być bezpieczne i idempotentne, a system powinien odróżniać zwykłą rywalizację o blokadę od awarii Redis.

W przypadku niepewnego stanu połączenia nie zakładaj, że blokada została uzyskana ani że jej nie uzyskano. Zaprojektuj przepływ tak, aby można go było bezpiecznie wznowić bez dwukrotnego wykonania nieodwracalnej czynności: na przykład użyj kluczy idempotencji w operacji biznesowej i przed jej ponowieniem sprawdź jej wynik.

Testy i sygnały operacyjne

Testuj zachowanie przy użyciu wielu workerów i kontroluj przeplatanie operacji, a nie tylko scenariusz pomyślny. Uwzględnij przypadki, w których proces kończy działanie przed zwolnieniem blokady, blokada wygasa w trakcie pracy, Redis nie odpowiada, a stary worker próbuje zwolnić blokadę po uzyskaniu jej przez inny proces. Sprawdź, czy token uniemożliwia usunięcie cudzego klucza i czy spóźnione zapisy są odrzucane, gdy aplikacja wymaga takiej gwarancji.

W środowisku produkcyjnym monitoruj nieudane próby uzyskania blokady, czas trwania sekcji krytycznych, wygaśnięcia, odnowienia, błędy połączeń i ponowienia. Rosnąca liczba konfliktów może wskazywać, że zakres blokady jest zbyt szeroki albo że wykonuje się w niej więcej pracy, niż to konieczne. Częste wygaśnięcia podczas zwykłego działania zadań sygnalizują, że należy zweryfikować czas trwania lease lub projekt procesu, a nie po prostu automatycznie zwiększać TTL.

Lista kontrolna ułatwiająca podjęcie decyzji

Lista kontrolna ułatwiająca podjęcie decyzji — guía visual de DedicatedPHP
  • Jaka reguła biznesowa jest chroniona i czy można ją wyrazić za pomocą ograniczenia lub transakcji w bazie danych?
  • Czy uzyskanie blokady jest atomowe i warunkowe oraz czy towarzyszy mu ustawienie czasu wygaśnięcia?
  • Czy każda próba ma unikalny token i czy przed usunięciem klucza podczas zwalniania sprawdzany jest właściciel?
  • Co się stanie, jeśli proces zostanie wstrzymany na dłużej niż lease albo Redis utraci połączenie?
  • Czy system odbierający zapis może odrzucać starsze wersje lub duplikaty?
  • Czy istnieje ograniczona polityka oczekiwania, ponawiania prób i odzyskiwania działania, a także metryki i testy współbieżności?

Redis zapewnia tymczasową koordynację, a nie uniwersalną gwarancję spójności. Używaj go wtedy, gdy problem rzeczywiście wymaga koordynowania procesów rozproszonych, a ostateczną ochronę pozostaw w systemie zapisującym dane. Jeśli regułę można prościej egzekwować w bazie danych, zacznij właśnie od tego.

Chcesz zastosować te pomysły w swoim projekcie?Omówmy Twoją platformę PHP.
Zobacz powiązaną usługę