Mikrousługi z komunikacją przez Axon

W tym prostym poradniku stworzymy kilka mikroserwisów na Spring Boot i zorganizujemy między nimi interakcję za pomocą frameworka Axon.

Mikrousługi z komunikacją przez Axon


Załóżmy, że mamy takie zadanie.

Mamy źródło transakcji na rynku finansowym. To źródło przesyła nam transakcje za pomocą interfejsu Rest.

Musimy te transakcje pobrać, zapisać w bazie danych i stworzyć wygodne pamięciowe przechowywanie.

To przechowywanie powinno wykonywać następujące funkcje:

  • zwracać listę transakcji;
  • zwracać pełne pozycje, tj. tabela „instrument” — „aktualna liczba papierów wartościowych”;
  • zwracać pozycję dla danego instrumentu.

Jak podejdziemy do rozwiązania tego zadania?

Zgodnie z zasadami mody mikroserwisowej, musimy podzielić zadanie na składowe mikroserwisy:

  • pobieranie transakcji przez Rest;
  • zapisywanie transakcji w bazie danych;
  • pamięć in-memory do przedstawiania danych o pozycjach.

Zróbmy w ramach tego poradnika pierwszy i trzeci serwis, zaś drugi zostawimy na drugą część (napiszcie w komentarzach, jeśli to interesujące).

I tak, mamy dwa mikroserwisy.

Pierwszy pobiera dane z zewnątrz.

Drugi przetwarza te dane i odpowiada na przychodzące zapytania.

Oczywiście chcemy uzyskać poziome skalowanie, aktualizacje bez przestojów i inne zalety mikroserwisów.

Jakie, dość złożone, zadanie przed nami stoi?

Tak naprawdę jest ich wiele, ale teraz porozmawiajmy o tym, jak będą przebiegać dane między tymi mikroserwisami. Można również stworzyć interfejs Rest, zainstalować jakąś kolejkę, można wymyślić wiele rzeczy z własnymi plusami i minusami.

Rozważmy jedno z możliwych podejść – asynchroniczną interakcję przez framework Axon..

Jakie są zalety tego rozwiązania?

Po pierwsze, asynchroniczna interakcja zwiększa elastyczność (tak, są tu też minusy, ale obecnie mówimy tylko o plusach).

Po drugie, otrzymujemy od razu Event Sourcing i CQRS.
Po trzecie, Axon dostarcza gotową infrastrukturę, więc musimy skupić się tylko na rozwoju logiki biznesowej.

Zaczynamy.

Nasz projekt będzie na gradle. Będzie zawierał trzy moduły:

  • common. moduł z wspólnymi strukturami danych (nie lubimy powielać kodu);
  • tradeCreator. moduł z mikroserwisem do przyjmowania transakcji przez Rest;
  • tradeQueries. moduł z mikroserwisem do wyświetlania pozycji.

Weźmiemy Spring Boot za podstawę i podłączymy starter Axon.

Axon działa doskonale i bez Spring, ale będziemy je używać razem.

W tym miejscu warto zatrzymać się i powiedzieć kilka słów o Axon.

To system klient-serwer. Istnieje serwer – to osobna aplikacja, którą uruchomimy w Dockerze.

A są klienci, którzy wbudowują się w mikrousługi.
Wygląda to następująco. Najpierw uruchamiany jest serwer Axon (w Dockerze), a potem nasze mikrousługi.

Podczas uruchamiania mikrousługi szukają serwera i zaczynają z nim współpracować. Współpraca może być w dużym uproszczeniu podzielona na dwa rodzaje: techniczną i biznesową.

Techniczna – to wymiana komunikatów „jestem żywy” (takie wiadomości można zobaczyć w trybie logowania debug).

Biznesowa – to wymiana wiadomości podobnych do „nowa transakcja”.

Ważną cechą jest to, że po uruchomieniu mikrousługi może zapytać serwer Axon „co się wydarzyło”, a serwer przekazuje mikrousłudze zgromadzone zdarzenia. Dzięki temu mikrousługa może być stosunkowo bezpiecznie uruchamiana ponownie bez utraty danych.
Przy takim schemacie wymiany możemy bardzo łatwo uruchamiać wiele egzemplarzy mikrousług,
i to na różnych hostach.

Tak, jeden egzemplarz serwera Axon – to nie jest niezawodne rozwiązanie, ale na razie tak to wygląda.

Pracujemy w ramach paradygmatów Event Sourcing i CQRS. To oznacza, że musimy mieć „komendy”, „zdarzenia” i „wybory”.

Będziemy mieć jedną komendę: „utwórz transakcję”, jedno zdarzenie „transakcja utworzona” oraz trzy wybory: „pokaż wszystkie transakcje”, „pokaż pozycję” i „pokaż pozycję według narzędzia”.

Schemat działania jest następujący:

  1. Mikrousługa tradeCreator odbiera transakcję przez Rest.
  2. Mikrousługa tradeCreator tworzy komendę „utwórz transakcję” i wysyła ją do serwera Axon.
  3. Serwer Axon przyjmuje komendę i przesyła ją do zainteresowanego odbiorcy, w tym przypadku do mikrousługi tradeCreator.
  4. Mikrousługa tradeCreator otrzymuje komendę, formuje zdarzenie „transakcja utworzona” i wysyła je do serwera Axon.
  5. Serwer Axon przyjmuje zdarzenie i przesyła je zainteresowanym subskrybentom.
  6. Obecnie mamy tylko jednego zainteresowanego odbiorcę – to mikrousługa tradeQueries.
  7. Mikrousługa tradeQueries otrzymuje zdarzenie i aktualizuje swoje wewnętrzne dane.

(Ważne jest, że w momencie formowania zdarzenia mikrousługa tradeQueries może być niedostępna, ale jak tylko się uruchomi, natychmiast otrzyma zdarzenie).

Tak, serwer Axon znajduje się w centrum komunikacji, wszystkie wiadomości przechodzą przez niego.

Przejdźmy do kodowania.

Aby nie zaśmiecać postu kodem, poniżej przedstawię tylko fragmenty, link do pełnego przykładu będzie poniżej.

Zacznijmy od modułu wspólnego common.

W nim znajdują się wspólne elementy – to zdarzenie (class CreatedTradeEvent). Zauważ, że nazwa jest w zasadzie nazwą komendy, która wygenerowała to zdarzenie, ale w czasie przeszłym. W czasie przeszłym, ponieważ najpierw pojawia się komenda, która prowadzi do stworzenia zdarzenia.

Inne wspólne struktury to klasy do opisu pozycji (class Position), transakcji (class Trade) oraz strony transakcji (enum Side), tj. kupno lub sprzedaż.

Przechodzimy do modułu tradeCreator.

Ten moduł ma interfejs Rest (class TradeController) do przyjmowania transakcji.
Z otrzymanej transakcji formułowana jest komenda „stwórz transakcję” i wysyłana do serwera axon.

    @PostMapping("/trade")
    public ResponseEntity create(@RequestBody Trade trade) {
        var createTradeCommand = CreateTradeCommand.builder()
                .tradeId(trade.getTradeId())
	...
                .build();
        var result = commandGateway.sendAndWait(createTradeCommand, 3, TimeUnit.SECONDS);
        return ResponseEntity.ok(result.get().toString());
    }

Do obsługi komendy używany jest klasy class TradeAggregate.
Aby Axon go znalazł, dodajemy adnotację @Aggregate.
Metoda do obsługi komendy wygląda tak (z skrótem):

    @CommandHandler
    public TradeAggregate(CreateTradeCommand command) {
        log.info("komenda: {}", command);
        var event = CreatedTradeEvent.builder()
                .tradeId(command.tradeId())
		....
                .build();
        AggregateLifecycle.apply(event);
    }

Z komendy formułowane jest zdarzenie i wysyłane na serwer.
Komenda znajduje się w klasie CreateTradeCommand.

Teraz spojrzymy na ostatni moduł tradeQueries.

Wywołania opisane są w pakiecie queries.
W tym module również znajduje się interfejs Rest
public class TradeController.

Na przykład, przyjrzyjmy się obsłudze zapytania: „pokaż wszystkie transakcje”.

    @GetMapping("/trade/all")
    public List findAllTrades() {
        return queryGateway.query(new FindAllTradesQuery(),
                ResponseTypes.multipleInstancesOf(Trade.class)).join();
    }

Tworzony jest wniosek o wybór i wysyłany na serwer.

Do obsługi wniosku o wybór używany jest klasy TradesEventHandler.
Zawiera on metodę, oznaczoną adnotacją

   @QueryHandler
    public List handleFindCurrentPositionQuery(FindCurrentPositionQuery query)

To właśnie ona odpowiada za selekcję danych z in-memory magazynu.

Pojawia się pytanie, jak w tym magazynie aktualizowane są informacje.

Zaczniemy od tego, że to po prostu zestaw ConcurrentHashMap, dostosowanych do konkretnych wyborów.
Do ich aktualizacji stosowana jest metoda:

    @EventHandler
    public void on(CreatedTradeEvent event) {
        log.info("event:{}", event);

        var trade = Trade.builder()
	...
                .build();
        trades.put(event.tradeId(), trade);
        position.merge(event.shortName(), event.size(),
                (oldValue, value) -> event.side() == Side.BUY ? oldValue + value : oldValue - value);
    }

Odbiera zdarzenie „transakcja utworzona” i aktualizuje Mapy.

To są główne punkty rozwoju mikrousług.

Co można powiedzieć o wadach Axona?

Po pierwsze, komplikuje to infrastrukturę, pojawia się punkt awarii – serwer Axon, cała komunikacja odbywa się przez niego.

Po drugie, wyraźnie manifestuje się wada podobnych rozproszonych systemów – czasowa niespójność danych. W naszym przypadku pomiędzy otrzymaniem nowej transakcji a aktualizacją danych dla zapytań może minąć nieakceptowalnie dużo czasu.

Co zostało pominięte?

Nic nie powiedziano o Event Sourcing i CQRS, co to takiego i do czego jest potrzebne.
Bez omówienia tych pojęć niektóre aspekty mogą być niejasne.

Możliwe, że niektóre fragmenty kodu również wymagają wyjaśnienia.

O tym porozmawiamy na otwartym webinarze 21 września.

Pełny przykład.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster