Co powinienem zrobić, gdy optymistyczne blokowanie nie działa?

11

Mam następujący scenariusz:

  1. Użytkownik wysyła żądanie GET/projects/1 i otrzymuje znacznik ETag .
  2. Użytkownik wysyła żądanie PUT do /projects/1ETag z kroku # 1.
  3. Użytkownik /projects/1przesyła kolejne żądanie PUT do ETag z kroku # 1.

Zwykle drugie żądanie PUT otrzymałoby odpowiedź 412, ponieważ ETag jest teraz nieaktualny - pierwsze żądanie PUT zmodyfikowało zasób, więc ETag już się nie zgadza.

Ale co, jeśli dwa żądania PUT zostaną wysłane w tym samym czasie (lub dokładnie jedno po drugim)? Pierwsze żądanie PUT nie ma czasu na przetworzenie i aktualizację zasobu przed przybyciem PUT nr 2, co powoduje zastąpienie PUT nr 1 przez PUT nr 2. Cały sens optymistycznego blokowania polega na tym, aby tak się nie stało ...

maximedupre
źródło
3
Atomizacja operacji w transakcjach na poziomie biznesowym, jak wyjaśnia Esben poniżej.
Robert Harvey
Co by się stało, gdybym rozpylił moje operacje przy użyciu transakcji? PUT nr 2 nie będzie przetwarzany, dopóki PUT nr 1 nie zostanie w pełni przetworzony?
maximedupre,
7
Zostań pesymistą?
jpmc26,
cóż, po to jest blokowanie.
Fattie,
Prawidłowo, oczywiście Put # 2 nie powinien być przetwarzany - powinny być wyjątkowe.
Fattie,

Odpowiedzi:

21

Mechanizm ETag określa tylko protokół komunikacyjny dla optymistycznego blokowania. Usługa aplikacji jest odpowiedzialna za wdrożenie mechanizmu wykrywania równoczesnych aktualizacji w celu wymuszenia optymistycznej blokady.

W typowej aplikacji korzystającej z bazy danych zwykle robisz to, otwierając transakcję podczas przetwarzania żądania PUT. Zwykle odczytujesz istniejący stan bazy danych w tej transakcji (aby uzyskać blokadę odczytu), sprawdzasz ważność Etag i zastępujesz dane (w sposób, który spowoduje konflikt zapisu, gdy dojdzie do jakiejkolwiek niezgodnej transakcji równoległej), następnie popełnij. Jeśli poprawnie skonfigurujesz transakcję, jeden z zatwierdzeń powinien zakończyć się niepowodzeniem, ponieważ oba będą próbowały jednocześnie aktualizować te same dane. Będziesz wtedy mógł użyć tej niepowodzenia transakcji, aby zwrócić 412 lub ponowić żądanie, jeśli ma to sens dla aplikacji.

Lie Ryan
źródło
Obecnie serwer implementuje mechanizm wykrywania równoczesnych aktualizacji poprzez porównywanie skrótów zasobu. Serwer używa również transakcji do wszystkich operacji, ale nie otrzymuję żadnych blokad, co może być przyczyną problemu. Jednak w twoim przykładzie, jak może wystąpić błąd w jednym z zatwierdzeń, jeśli transakcje używają blokad? Druga transakcja powinna być w trakcie oczekiwania na odczyt stanu, dopóki pierwsza transakcja nie zostanie rozwiązana.
maximedupre,
1
@maximedupre: jeśli korzystasz z transakcji, masz jakieś blokady, chociaż mogą to być blokady niejawne (blokady są uzyskiwane automatycznie, gdy czytasz / aktualizujesz pola, a nie jest to wyraźnie wymagane). Mechanizm, który opisałem powyżej, można wdrożyć za pomocą tych ukrytych blokad. Drugie pytanie zależy od używanej bazy danych, ale wiele nowoczesnych baz danych korzysta z MVCC (kontrola współbieżności wielu wersji), aby umożliwić wielu czytnikom i programowi zapisującemu pracę na tych samych polach bez niepotrzebnego wzajemnego blokowania.
Lie Ryan,
1
Ostrzeżenie: w wielu systemach DBMS (PostgreSQL, Oracle, SQL Server itp.) Domyślnym poziomem izolacji transakcji jest „zatwierdzony odczyt”, gdzie podejście nie wystarcza, aby zapobiec wyścigowi PO. W takich DMBS możesz to naprawić, włączając do klauzuli AND ETag = ...swojego UPDATEoświadczenia WHEREi sprawdzając później zaktualizowaną liczbę wierszy. (Lub stosując bardziej rygorystyczny poziom izolacji transakcji, ale tak naprawdę nie polecam.)
ruakh
1
@ruakh: zależy to od sposobu pisania zapytania, tak domyślny poziom izolacji nie zapewnia takiego zachowania automatycznie dla wszystkich zapytań, ale często możliwe jest ustrukturyzowanie transakcji w sposób wystarczający do wdrożenia optymistycznego blokowania. W większości przypadków, jeśli spójność transakcji jest ważna w aplikacji, zaleciłbym powtarzalny odczyt jako domyślny poziom izolacji; w bazach danych korzystających z MVCC narzut powtarzalnego odczytu jest dość minimalny i znacznie upraszcza aplikację.
Lie Ryan,
1
@ruakh: główną wadą powtarzalnego odczytu jest to, że musisz być przygotowany na ponowienie próby lub niepowodzenie, jeśli istnieje jednoczesna transakcja. Jest to zwykle problem, ale aplikacje, które zapewniają optymistyczne blokowanie jako strategię współbieżności, i tak będą już wymagały tej obsługi, więc powtarzalne błędy odczytu są mapowane naturalnie na optymistyczne błędy blokowania, a to tak naprawdę nie dodaje nowych wad.
Lie Ryan,
13

Musisz wykonać atomowo następującą parę:

  • sprawdzanie poprawności znacznika (tj. jest aktualne)
  • aktualizowanie zasobu (w tym aktualizowanie jego znacznika)

Inni nazywają to transakcją - ale zasadniczo atomowe wykonanie tych dwóch operacji uniemożliwia zastąpienie drugiej przypadkowym przypadkiem; bez tego masz wyścig, jak zauważasz.

Jest to nadal uważane za optymistyczne blokowanie, jeśli spojrzysz na duży obraz: że sam zasób nie jest blokowany przez początkowy odczyt (GET) przez dowolnego użytkownika lub użytkowników, którzy patrzą na dane, czy to z zamiarem aktualizacji, czy nie.

Niektóre zachowania atomowe są konieczne, ale dzieje się to w ramach pojedynczego żądania (PUT), a nie próby zablokowania wielu interakcji sieciowych; jest to optymistyczne blokowanie: obiekt nie jest blokowany przez GET, ale nadal można go bezpiecznie zaktualizować za pomocą PUT.

Istnieje również wiele sposobów na atomowe wykonanie tych dwóch operacji - zablokowanie zasobu nie jest jedyną opcją; na przykład może wystarczyć lekka blokada wątku lub obiektu i zależy ona od architektury aplikacji i kontekstu wykonania.

Erik Eidt
źródło
4
+1 za zauważenie, że liczy się atomowość. W zależności od aktualizowanego zasobu podstawowego można to zrobić bez transakcji lub blokowania. Na przykład atomowe porównywanie i zamiana zasobu w pamięci lub pozyskiwanie zdarzeń utrwalonych danych.
Aaron M. Eshbach,
@ AaronM.Eshbach, zgodził się i dziękuje za zaproszenie.
Erik Eidt,
1

Twórca aplikacji musi sprawdzić E-Tag i podać tę logikę. Nie jest magią, że serwer WWW robi to za Ciebie, ponieważ wie tylko, jak obliczyć E-Tagnagłówki dla zawartości statycznej. Weźmy więc powyższy scenariusz i opiszmy, jak powinna wyglądać interakcja.

GET /projects/1

Serwer otrzymuje żądanie, określa E-Tag dla tej wersji rekordu, zwracając go wraz z faktyczną zawartością.

200 - OK
E-Tag: "412"
Content-Type: application/json
{modified: false}

Ponieważ klient ma teraz wartość E-Tag, może to uwzględnić w PUTżądaniu:

PUT /projects/1
If-Match: "412"
Content-Type: application/json
{modified: true}

W tym momencie aplikacja musi wykonać następujące czynności:

  • Sprawdź, czy E-Tag jest nadal poprawny: „412” == „412”?
  • Jeśli tak, dokonaj aktualizacji i oblicz nowy E-Tag

Wyślij odpowiedź powodzenia.

204 No Content
E-Tag: "543"

Jeśli nadejdzie inne żądanie i spróbuje wykonać PUTpodobne do powyższego, za drugim razem, gdy kod serwera je oceni, będziesz odpowiedzialny za przesłanie komunikatu o błędzie.

  • Sprawdź, czy znacznik E jest nadal poprawny: „412”! = „543”

W przypadku awarii wyślij odpowiedź awarii.

412 Precondition Failed

To jest kod, który musisz napisać. E-Tag może w rzeczywistości być dowolnym tekstem (w granicach określonych w specyfikacji HTTP). To nie musi być liczba. Może to być również wartość skrótu.

Berin Loritsch
źródło
Nie używasz tutaj standardowej notacji HTTP. W standardowym HTTP zgodnym ze standardem ETag używa się tylko w nagłówku odpowiedzi. Nigdy nie wysyłasz ETag w nagłówku żądania, ale zamiast tego używasz poprzednio uzyskanej wartości ETag w nagłówku If-Match lub If-None-Match w nagłówkach żądania.
Lie Ryan,
-2

Jako uzupełnienie innych odpowiedzi opublikuję jeden z najlepszych cytatów w dokumentacji ZeroMQ, który wiernie opisuje podstawowy problem:

Aby stworzyć całkowicie doskonałe programy MT (i mam na myśli to dosłownie), nie potrzebujemy muteksów, blokad ani żadnej innej formy komunikacji między wątkami, z wyjątkiem wiadomości wysyłanych przez gniazda ZeroMQ.

Przez „doskonałe programy MT” rozumiem kod, który jest łatwy do napisania i zrozumienia, który działa z tym samym podejściem projektowym w dowolnym języku programowania i na dowolnym systemie operacyjnym, i który skaluje się na dowolnej liczbie procesorów z zerowymi stanami oczekiwania i bez sensu malejących zysków.

Jeśli spędziłeś lata na nauce sztuczek, aby Twój kod MT w ogóle działał, a tym bardziej szybko, dzięki blokadom, semaforom i sekcjom krytycznym, będziesz zniesmaczony, gdy zdasz sobie sprawę, że to wszystko na nic. Jeśli jest jedna lekcja, którą wyciągnęliśmy z ponad 30 lat równoczesnego programowania, to: po prostu nie udostępniaj stanu. To jak dwóch pijaków próbujących wypić piwo. Nie ma znaczenia, czy są dobrymi kumplami. Wcześniej czy później zamierzają walczyć. Im więcej pijaków dodasz do stołu, tym bardziej walczą ze sobą o piwo. Tragiczna większość aplikacji na MT wygląda jak bójka po pijanemu.

lurscher
źródło