Architektura zdarzeniowa (event‑driven) – kolejki, strumienie i idempotencja,

Architektura zdarzeniowa (event‑driven) – kolejki, strumienie i idempotencja,

 

Architektura zdarzeniowa (Event-Driven Architecture, EDA) to paradygmat projektowania systemów, w którym komunikacja między komponentami odbywa się poprzez zdarzenia. Zamiast bezpośredniego wywoływania usług, komponenty emitują zdarzenia, które następnie są konsumowane przez inne, zainteresowane komponenty. Ta luźna forma powiązania (loose coupling) jest fundamentalną cechą, która odróżnia EDA od tradycyjnej architektury opartej na żądaniach i odpowiedziach. To podejście zapewnia elastyczność, skalowalność i odporność na awarie, pozwalając na tworzenie złożonych, rozproszonych systemów.

Podstawowe koncepcje i komponenty

W sercu architektury zdarzeniowej leżą trzy kluczowe koncepcje: producenci, konsumenci i kanał komunikacji.

Producenci (Producers) to komponenty, które generują zdarzenia. Zdarzenie to informacja o tym, że coś się wydarzyło, np. „ZamówienieZłożone”, „UżytkownikZarejestrowany”, „PłatnośćOdrzucona”. Producenci nie muszą wiedzieć, kto i w jaki sposób wykorzysta ich zdarzenia. Ich jedynym zadaniem jest opublikowanie zdarzenia w kanale komunikacji.

Konsumenci (Consumers) to komponenty, które subskrybują kanał komunikacji i reagują na zdarzenia, które ich interesują. Konsument przetwarza zdarzenie i wykonuje na jego podstawie jakąś akcję, np. wysyła e-mail potwierdzający zamówienie lub aktualizuje stan magazynowy. Konsument nie musi wiedzieć, kto wyprodukował zdarzenie.

Kanał komunikacji to mechanizm, który pośredniczy w komunikacji między producentami a konsumentami. Najczęściej występują w dwóch głównych formach: kolejek (queues) i strumieni (streams).

Kolejki (np. RabbitMQ, SQS) działają na zasadzie punkt-punkt (point-to-point). Zdarzenie jest wysyłane do kolejki, a stamtąd pobierane przez jednego konsumenta. Po przetworzeniu zdarzenia jest ono usuwane z kolejki. To idealne rozwiązanie dla zadań, które muszą być wykonane dokładnie raz, np. wysłanie e-maila.

Strumienie (np. Apache Kafka, Amazon Kinesis) działają na zasadzie publikacja-subskrypcja (publish-subscribe). Zdarzenie jest publikowane w strumieniu, a następnie może być konsumowane przez wielu różnych konsumentów, niezależnie od siebie. Dodatkowo, zdarzenia w strumieniu są persystowane, co pozwala na ich ponowne przetwarzanie lub odtwarzanie historii zdarzeń. To podejście jest idealne dla systemów, które wymagają audytu lub wielu niezależnych reakcji na to samo zdarzenie.

Idempotencja: klucz do niezawodności

W systemach rozproszonych, gdzie komunikacja jest asynchroniczna, zawsze istnieje ryzyko, że zdarzenie zostanie przetworzone więcej niż raz. Może to wynikać z awarii konsumenta w trakcie przetwarzania, problemów z siecią lub innych nieprzewidzianych błędów. Aby uniknąć niespójności i błędów logicznych, kluczowe jest, aby operacje były idempotentne.

Idempotencja to cecha operacji, która oznacza, że jej wielokrotne wywołanie daje taki sam efekt, jak pojedyncze wywołanie.

Przykładowo, operacja ustaw_status_zamowienia_na_wyslane(zamowienie_id) jest idempotentna, ponieważ wielokrotne jej wywołanie nie zmieni stanu zamówienia po pierwszym poprawnym wykonaniu.

Natomiast operacja dodaj_5_dolarow_do_salda(uzytkownik_id) nie jest idempotentna, ponieważ każde kolejne wywołanie zwiększy saldo. Jeśli takie zdarzenie zostanie przetworzone dwukrotnie, saldo użytkownika będzie niepoprawne.

Aby zapewnić idempotencję, programiści mogą stosować różne techniki:

  • Unikalne identyfikatory zdarzeń (Event IDs): Każde zdarzenie powinno mieć unikalny identyfikator. Konsument przed przetworzeniem zdarzenia powinien sprawdzić, czy dany identyfikator nie został już przetworzony. Może to być zrealizowane za pomocą bazy danych, która przechowuje listę przetworzonych ID.
  • Stanowe operacje: Zamiast operacji „zwiększ”, lepiej używać „ustaw”. Zamiast zwiększ_saldo_o_5, lepiej używać ustaw_saldo_na_105. Konsument musi jedynie mieć pewność, że jego operacja jest ostatnią dla danego stanu.
  • Optymistyczna blokada: W niektórych przypadkach, gdy konieczna jest inkrementacja, można używać numerów wersji lub znaczników czasu. Konsument przed wykonaniem operacji sprawdza wersję obiektu, dokonuje zmian i próbuje zaktualizować obiekt, ale tylko wtedy, gdy jego wersja jest wciąż aktualna.

Architektura zdarzeniowa oferuje potężne narzędzia do budowy elastycznych i skalowalnych systemów. Przejście od bezpośredniej komunikacji do modelu opartego na zdarzeniach pozwala na luźne powiązanie komponentów i budowanie systemów odpornych na awarie. Kluczowym wyborem jest decyzja między kolejkami a strumieniami, w zależności od wymagań co do konsumpcji zdarzeń. Niezależnie od wybranego kanału komunikacji, kluczowym aspektem, który zapewnia spójność i niezawodność, jest projektowanie operacji w sposób idempotentny. Tylko w ten sposób można zagwarantować, że nawet w obliczu awarii i wielokrotnego przetwarzania zdarzeń, system pozostanie w poprawnym stanie. Opanowanie tych koncepcji to fundamentalny krok w stronę tworzenia nowoczesnych, rozproszonych aplikacji, które są w stanie sprostać wyzwaniom współczesnego świata.

Face 4
Mirek Drzewiecki

Jestem programistą z wieloletnim doświadczeniem w branży IT. Od zawsze fascynują mnie nowe technologie, a moją misją jest dzielenie się wiedzą i pomaganie innym developerom w rozwoju. Na co dzień tworzę poradniki, analizuję trendy i testuję narzędzia, które ułatwiają pracę programistom. Uważam, że ciągłe doskonalenie umiejętności oraz wymiana doświadczeń to klucz do sukcesu w świecie technologii.