Java Developer? Przejdź na wyższy poziom wiedzy 🔥💪  Sprawdź

Team Leader? Podnieś efektywność swojego zespołu 👌 Sprawdź

The Outbox Pattern – Komunikacja z zewnętrznymi systemami w ramach transakcji

utworzone przez 13 października 2022Hibernate, Java

Często w ramach przetwarzania logiki biznesowej, oprócz modyfikacji różnych danych objętej transakcją pojawia się potrzeba wymiany danych lub komunikacji z zewnętrznymi systemami. Przykładem jest serwer mailowy, inny serwis, system kolejkowy.

Wykonując operację z efektem ubocznym w transakcji, mogą wystąpić dwa potencjalne problemy:

  • Możesz nie mieć wpływu na działanie tych systemów. Na przykład serwer pocztowy nie odpowiada, pojawia się chwilowy zanik sieci, albo bardzo wolno odpowiada).
  • Wykonanie efektu ubocznego i wycofanie transakcji biznesowej. Nie można już odwrócić efektu uczonego (np. nie można wycofać już wysłanego maila).

Jak można sobie z tym poradzić? W tym wpisie opiszę wzorzec The Outbox Pattern.

Efekty uboczne w transakcji

Pisząc coraz bardziej skomplikowane oprogramowanie, komunikujące się z zewnętrznymi systemami z czasem pojawiają się problemy, na które możesz jedynie reagować.

Process order and send email

W ramach własnego oprogramowania możesz wprowadzić mechanizmy zabezpieczające przed błędami np.:

  • Transakcja opakowująca ciąg operacji, aby wszystkie zmiany danych były zatwierdzone jednocześnie, albo w przypadku błędu następuje wycofanie wszystkich dotychczas dokonanych zmian,
  • Możesz powtarzać pewne operacje (Retry Pattern, Circuit Breaker) do skutku
  • Możesz zabezpieczyć się przed zduplikowanym przetwarzaniem (idempotency check)

Możesz nie mieć natomiast większego wpływu np. na działanie serwera pocztowego czy stabilność sieci.

Innym problemem jest to, że może wystąpić efekt uboczny, którego nie ma możliwości wycofać, np. wysłanie emaila albo efekt w innym systemie. Wyobraź sobie, że został wysłany email, a transakcja biznesowa wycofała się. Nie można cofnąć wysłania emaila.

Na powyższym przykładzie wysyłka maila jest po zatwierdzeniu transakcji (czyli po tym – na co masz wpływ).

Ale może w tym przypadku wystąpić sytuacja, w której mail nie zostanie wysłany albo serwer mailowy nie zwróci na czas odpowiedzi i wystąpi wyjątek typu timeout (a transakcja została zatwierdzona). Co wtedy możesz zrobić:

  • Powtarzać wysyłkę – ale mogą się zdarzyć duplikaty maili, albo wielokrotne SMSy (za które zostaniesz obciążony)
  • Można dodać w aplikacji opcję ponownej wysyłki na żądanie (klient dzwoni, operator wchodzi do systemu i ponawia wysyłkę 'z guzika’)

Two-phase commit protocol (2PC)

Istnieją pewne rozwiązania pozwalające na tworzenie rozproszonej transakcji gwarantującej atomowość takiej operacji (np. protokół 2 Phase Commit).

Two Phase Commit

Powyżej uproszczony schemat działania. Pojawiają się elementy (komponenty) takie jak koordynator oraz rozproszone usługi (Service1 i Service 2).

  • Koordynator – koordynuje rozproszoną transakcję (start, pre commit, i zakończenie jako commit/rollback)
  • Service1, Service 2 – rozproszone komponenty (usługi, serwery, baza danych etc.) – muszą współpracować z koordynatorem i implementować w odpowiedni sposób pre commit, commit i rollback

Wymaga on sporego nakładu prac, oprogramowania wycofywania transakcji i co ważne wszystkie elementy muszą to wspierać.

A jak np. w takim układzie wycofać wysłanie maila?

Oprócz tego pojawia się mocne powiązanie komponentów (coupling) – np. bez koordynatora nic nie będzie działać.

Kolejna wada to długi czas przetwarzania – komunikacja w sieci, potwierdzenia etc.

Jeśli tworzysz jakiś system, to musi gwarantować akceptowalny czas przetwarzania

  • Współczesny użytkownik nie będzie czekał 30s na zatwierdzenie operacji (będzie raczej niezadowolony)
  • Przeważnie ograniczasz maksymalny czas transakcji i może pojawić się timeout
Zniecierpliwiony użytkownik

Psst… Interesujący artykuł?

Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.

.

Fire-and-forget

Wszystko to może powodować, że tradycyjne 'szeregowe’ przetwarzanie staje się nieprzydatne – pojawia się potrzeba wykonywania operacji typu uruchom i zapomnij (fire and forget). I najlepiej gdyby te operacje były bardzo szybkie, a jakiś inny mechanizm w tle zajął się wykonywaniem 'w tle’ logiki związanej z komunikacją z jakimś zewnętrznym komponentem.

Send email – fire and forget

Możesz użyć przetwarzania asynchonicznego w osobnym wątku.

@Configuration
@EnableAsync
@EnableRetry
public class EmailSenderConfig { ... }
...

public class EmailSender {
    private final RetryableEmailApi api;

    @Async 
    public void sendEmail(EmailRequest request) {
     ...
     api.doSend(request.toData()); 
    }
}

public class RetryableEmailApi {
   @Retryable(value = LateHourException.class, maxAttempts = 2, backoff = @Backoff(delay = 100))
   public void doSend(EmailData emailData) {
   ....
   }
}

Ale co się stanie jeśli Twój mikroserwis zostanie zatrzymany – operacje nie zostaną wykonane.

Pojawia się potrzeba 'jakiejś’ persystencji żądań komunikacji z zewnętrznymi systemami i przetwarzania w jakiś uporządkowany (kolejność) sposób. Tak, aby nawet w przypadku restartu, awarii Twojego komponentu/mikroserwisu po przywróceniu działania system kontynuował od miejsca, w którym skończył przed awarią.

The Outbox Pattern

Możesz wykorzystać wspomniany wcześniej outbox pattern zwany też transactional outbox.

Jak to działa:

  • Logika biznesowa objęta jest transakcją.
  • W ramach logiki biznesowej tworzone jest żądanie wykonania operacji, które następnie jest zapisywane w bazie w ramach tej samej transakcji. Jest ono bardzo szybkie (nie musisz czekać na wysłanie maila), co oznacza znacznie krótszą i pewniejszą transakcję.
  • Transakcja kończy się i w przypadku zatwierdzenia (commit) pozostaje rekord żądania w tabeli (outbox table – w przykładzie poniżej EMAIL_NOTIFICATION).
Transactional oubox – create request
  • Osobny komponent (proces) w osobnym cyklu życia (inny wątek, zadanie typu cron) przegląda żądania i je wykonuje, odpowiednio aktualizując stan tabeli żądań (sam obsługuję powtórzenia etc.).
Transactional outbox – process delivery
  • Ten osobny komponent zwiększa decoupling i niezależność.
    • Możesz w dowolny sposób sterować powtórzeniami (at-least-once, at-most-once, retry)
    • Możesz np. nie powtarzać wysyłki SMSów w przypadku błędu odpowiedzi bramki, aby nie narazić na zbędne koszty (może jednak SMS wyszedł i zostaniesz za to obciążony)
    • Możesz wstrzymać wysyłkę SMSów w nocy, aby nie budzić klienta

Uwaga. Jeśli Twój mikroserwis ma wiele instancji musisz zapewnić dodatkowy mechanizm blokujący wykonanie żądania dla pojedynczego żądania tylko na jednym węźle – np. optimistic/pessimistic locking, framework Quartz.

Główną zaletą The Outbox Pattern jest to, że zlecenie operacji występuje w transakcji biznesowej, a wykonanie w innej, mniejszej transakcji działającej w tle, która oznacza zadanie jako ukończone.

Psst… Interesujący artykuł?

Jeżeli podoba Ci się ten artykuł i chcesz takich więcej – dołącz do newslettera. Nie ominą Cię materiały tego typu.

.

Alternatywne podejście – publikacja eventu

Możesz się przed tym zabezpieczyć – używając zdarzeń i zamiast tabeli outbox skorzystać np. z Kafka i eventu typu order_created.

Outbox pattern with Kafka

Wówczas będziesz miał gwarancję, że tylko jeden węzeł skonsumuje dane zdarzenie i wyśle mail. A w przypadku błędu wysyłki w odpowiedni sposób go obsłuży (np. kolejka powtórzeń retry queue).

Jeśli Twój komponent (producent) wykorzysta tryb at-least-once przy publikacji komunikatów (eventu order_created) do Kafka, musisz zapewnić obsługę idempotencji, aby nie dublować wysyłki maili.

Można by powiedzieć, że przecież Kafka jest osobnym zewnętrznym komponentem. Jednak w przypadku architektury wykorzystującej wewnętrznie Kafka zakłada się, że jest ona częścią architektury Twojego mikroserwisu – tak jak baza danych czy dysk serwera.

Tutaj także istnieje minimalne ryzyko, iż wysyłka eventu na Kafka się nie powiedzie. Jeżeli chcesz mieć absolutną pewność, że event został wysłany na Kafka to możesz wykorzystać tabelę outbox z logiką wysyłki na Kafka w osobnym procesie.

Podoba Ci się ten artykuł? Weź więcej.

Jeżeli uważasz ten materiał za wartościowy i chcesz więcej treści tego typu – nie przegap ich i otrzymuj je prosto na swoją skrzynkę. Nawiążmy kontakt.

.

Apache Kafka – wydajność vs. gwarancja dostarczenia wiadomości

Jak stworzyć piekielnie szybką albo maksymalnie bezpieczną wersję producenta oraz konsumenta.

Przydatne linki

  • https://softwareskill.pl/retry-pattern
  • https://softwareskill.pl/circuit-breaker-pattern
  • https://martinfowler.com/articles/patterns-of-distributed-systems/two-phase-commit.html
  • https://softwareskill.pl/konsument-wiadomosci-w-apache-kafka
  • https://intellitect.com/blog/decoupling-csharp-testable/
  • https://www.baeldung.com/spring-async
  • http://www.kamilgrzybek.com/design/the-outbox-pattern/
  • https://microservices.io/patterns/data/transactional-outbox.html
  • https://event-driven.io/en/outbox_inbox_patterns_and_delivery_guarantees_explained/
  • https://softwaremill.com/microservices-101/
  • https://www.squer.at/en/blog/stop-overusing-the-outbox-pattern/
  • Obraz autorstwa yanalya na Freepik na Freepik

Gdybyś potrzebował jeszcze więcej:

Jesteś Java Developerem?

Przejdź na wyższy poziom wiedzy 
„Droga do Seniora” 🔥💪

Jesteś Team Leaderem? Masz zespół?

Podnieś efektywność i wiedzę swojego zespołu 👌

Czym jest The Outbox Pattern?

Outbox pattern rozwiązuje problemy komunikacji z zewnętrznymi usługami typu wyślij i zapomnij poprzez rozdzielenie logiki przetwarzania danych od logiki komunikacji z systemem zewnętrznymi (osobne procesy z tabelą żądań komunikacji).

Czym jest Two phase commit (2PC)?

Two phase commit (2PC) pozwala na stworzenia rozproszonej transakcji obejmującej wiele różnych komponentów (nie tylko baza danych) i gwarantującej atomowość takiej operacji (commit lub rollback 'u wszystkich’).

Dyskusja