Oskar Dudycz

Pragmatic about programming

Optymistyczny email na ciekawe czasy

2020-03-23 oskar dudyczWzorce

cover

Cześć!

Podobno najgorsze co można komuś życzyć, to żeby żył w ciekawych czasach. W tych właśnie naszych ciekawych czasach chciałem napisać coś ku poprzepieniu serc. Coś optymistycznego. Co może być bardziej optymistycznego niż optymistyczna współbieżność(Optimistic Concurrency)? Suchar. No ale do tego pewnie już Cię przyzwyczaiłem.

Dlaczego więc ta nasza współbieżność jest optymistyczna? Jest taka, bo zakłada, że w naszym systemie sytuacje gdy ktoś będzie próbował edytowąć ten sam zasób (rekord) będą należały do rzadkości. Co w większości kategorii systemów się w zasadzie sprawdza. Zwykle jednocześnie jedna osoba na raz edytuje zamówienie, polisę, swoje dane itd. Dlatego najczęściej możemy optymistycznie założyć, że sytuacje gdy więcej jedna osoba będzie chciała zmienić ten sam rekord będą rzadkością.

Czy takie założenie oznacza, że kto pierwszy ten lepszy i że w zasadzie to yolo, każdy nadpisuje każdego? Bynajmniej. Tzn. prawie bynajmniej. Faktycznie jest tak, że kto pierwszy dostanie się do bazy/serwisu ten lepszy, bo jego zapis się powiedzie. Drugi się wywali z informacją, że ktoś w między czasie dokonał zmiany na tym zasobie. Niezbyt przyjemna rzecz, ale tak jak pamiętamy to nasz rodzaj współbieżności jest optymistyczny - założeniem jest to, że taka sytuacja będzie bardzo rzadka i możemy sobie pozwolić jak już nastanie na “grubsze” środki.

Skoro mamy podejście optymistyczne, to pewnie mamy i nasze typowe polskie, czyli pesymistyczne? No mamy, a czym te dwa podejścia się różnią? Pesymistyczne zakłada, że istnieje układ, który chce zepsuć nasz zasób i jeśli tylko na chwilę spuścimy go z oka to coś złego na nim zrobi. Dlatego też dla zasady jak tylko otworzymy tryb edycji czy też w ogóle otworzymy rekord to zakładamy na nim blokadę (lock), która unimożliwia innym edycję dopóki nie powiemy łaskawie “no teraz już możesz”. Często się to spotyka np. w systemach bankowych czy innych, gdzie wchodzą w grę finanse. Ogólnie podnosimy i opuszczamy semafor.

Optymistyczne podejście ma tę przewagę, że odczyty są dozwolone w każdym momencie, nie ma tutaj żadnych ograniczeń. Zapisy są tak jak powyżej.

Te podejście ma też bardzo ważną cechą biznesową. Zakłada, że chcemy podejmować decyzje w naszym systemie na podstawie najświeższych przesłanek. Jeśli chcemy dokonać zmianę naszego zasobu, a on w między czasie się zmienił to system sam zapyta nas o to, “czy na pewno chcemy, bo teraz dane są już inne”.

Jak zatem wygląda implementacja optimistic concurrency?

  1. Przy odczycie wraz z rekordem odczytaj bieżącą wersję zasobu.
  2. Zmodyfikuj rekord i wyślij go wraz z wersją (niezmienioną).
  3. Serwer/baza pobiera wersję aktualną wersję zasobu.
  4. Sprawdź czy wersje są te same.
  5. Jeśli nie to rzuć błędem/zwróc kod błędu.
  6. Jeśli tak to pozwól na zapis, dokonaj go i zmień wersję (np. zinkrementuj).

To co ważne, to wersja wcale nie musi być numerem. Bardzo często jest, ale nie zawsze. Dlaczego? Bo nie ma konieczności weryfikacji czy numer jest większy, mniejszy, czy różni się o jeden. To co wystarczy do sprawdzenia to czy wersja jest ta sama. W systemach rozproszonych jest bardzo trudne uzyskanie globalnego numeru inkrementowanego za każdym razem (w każdym razie bez negatywnego wpłuwu na wydajność). Dlatego bardzo często używa się jako wersję Guidów. Dzięki czemu po zmianie rekordu przypisujemy nowy losowy Guid jako nową wersję. Dzięki czemu nie musimy robić globalnej synchronizacji numerów. Upraszcza to znacząco rozwiązanie. Guidy są używane jako wersje dokumentów w Marten (zobacz więcej: https://martendb.io/documentation/documents/advanced/optimistic_concurrency/). Entity Framework wspiera ją tak: https://docs.microsoft.com/en-us/ef/core/modeling/concurrency?tabs=data-annotations

Właśnie tez w systemach rozproszonych optymistyczna współbieżność pokazuje swoją moc. W takich systemach jest bardzo trudno zapewnić sztywną spójność danych. Stosowanie optimistic concurrency wraz z algorytmami procesowania rozproszonego pozwala na łatwiejsze obsługę i symulowanie tranzakcji. Tak właśnie podchodzą do tego rozproszone z założenia bazy danych:

Jak to obsłużyć w API webowym? Najczęstszym podejściem jest przesłanie wersji jako header Etag (https://en.wikipedia.org/wiki/HTTP_ETag), zwracając status 409 (https://http.cat/409) w przypadku gdy próbujemy zmodyfikować zasób, który w między czasie ktoś już uaktualnił.

Użycie Optimistic Concurrency jest też podstawą w zapewnieniu kolejności zdarzeń w Event Sourcingu. Zachęcam do przerobienia zadania z mojego “Self Paced Kit” (https://github.com/oskardudycz/EventSourcing.NetCore/tree/master/Workshop):

To naprawdę jest tak proste. W Marten implementacja produkcyjna wygląda niemalże bliźniaczo: https://github.com/JasperFx/marten/blob/master/src/Marten/Events/AppendEventFunction.cs#L43

Brak optimistic locking jest też jednym z powodów, dla których Kafka to nie jest narzędzie do Event Sourcingu, więcej: https://issues.apache.org/jira/browse/KAFKA-2260

Masz jakieś pytania? (Nie) podobało Ci się? Napisz!

Trzymaj się zdrowo i optymistycznie!

p.s. na koniec kącik muzyczny. Również Optimistic. Wiesz, że kiedyś prowadziłem największy portal w polsce o Radiohead?

  • © Oskar Dudycz 2019-2020