Mikroservices mit Kommunikation über Axon

In diesem einfachen Tutorial erstellen wir ein paar Mikrodienste mit Spring Boot und organisieren die Interaktion zwischen ihnen über das Axon-Framework.

Mikroservices mit Kommunikation über Axon


Angenommen, wir haben folgende Aufgabe.

Es gibt eine Quelle für Transaktionen an der Börse. Diese Quelle überträgt uns Transaktionen über eine REST-Schnittstelle.

Wir müssen diese Transaktionen abrufen, in einer Datenbank speichern und einen bequemen In-Memory-Speicher erstellen.

Dieser Speicher sollte folgende Funktionen erfüllen:

  • eine Liste von Trades zurückgeben;
  • die vollständige Position zurückgeben, d.h. eine Tabelle „Instrument“ — „aktueller Bestand“;
  • die Position für ein bestimmtes Instrument zurückgeben.

Wie gehen wir an diese Aufgabe heran?

Entsprechend den Prinzipien der Mikrodienstarchitektur müssen wir die Aufgabe in separate Mikrodienste unterteilen:

  • Abrufen der Transaktion über REST;
  • Speichern der Transaktion in der Datenbank;
  • In-Memory-Speicher zur Darstellung der Daten zur Position.

Lass uns im Rahmen dieses Tutorials den ersten und dritten Dienst erstellen, den zweiten lassen wir für den zweiten Teil (schreib in die Kommentare, wenn das interessant ist).

Also, wir haben zwei Mikrodienste.

Der erste erhält Daten von außen.

Der zweite verarbeitet diese Daten und antwortet auf eingehende Anfragen.

Wir möchten natürlich horizontale Skalierung, unterbrechungsfreies Update und andere Vorteile von Mikrodiensten erhalten.

Welche, durchaus anspruchsvolle, Aufgabe steht uns bevor?

Es gibt viele, aber lass uns jetzt darüber sprechen, wie die Daten zwischen diesen Mikrodiensten fließen werden. Zwischen ihnen kann man auch REST machen, man kann eine Warteschlange einrichten, es gibt viele Möglichkeiten mit ihren Vor- und Nachteilen.

Lass uns einen der möglichen Ansätze betrachten – asynchrone Interaktion über das Axon-Framework.

Welche Vorteile bietet diese Lösung?

Erstens, asynchrone Interaktion erhöht die Flexibilität (ja, es gibt hier auch einen Nachteil, aber wir sprechen hier vorerst nur über die Vorteile).

Zweitens, wir erhalten direkt out of the box Event Sourcing und CQRS.
Drittens, Axon bietet eine fertige Infrastruktur, sodass wir uns nur auf die Entwicklung der Geschäftslogik konzentrieren müssen.

Legen wir los.

Unser Projekt wird mit Gradle sein. Es wird drei Module geben:

  • common. Modul mit gemeinsamen Datenstrukturen (wir mögen kein Kopieren);
  • tradeCreator. Modul mit diesem Mikrodienst zur Annahme von Transaktionen über REST;
  • tradeQueries. Modul mit dem Mikrodienst zur Anzeige der Position.

Wir nehmen Spring Boot als Grundlage und verbinden den Axon-Starter.

Axon funktioniert hervorragend auch ohne Spring, aber wir werden sie gemeinsam nutzen.

Hier müssen wir innehalten und ein paar Worte über Axon sagen.

Es handelt sich um ein Client-Server-System. Es gibt einen Server – das ist eine separate Anwendung, die wir in Docker ausführen werden.

Und es gibt Clients, die in Mikrodienste integriert sind.
So ergibt sich folgendes Bild: Zuerst wird der Axon-Server (in Docker) gestartet, dann unsere Mikrodienste.

Beim Start suchen die Mikrodienste den Server und beginnen, mit ihm zu interagieren. Diese Interaktion lässt sich grob in zwei Arten unterteilen: technische und geschäftliche.

Technisch ist das der Austausch von Nachrichten wie „Ich bin am Leben“ (solche Nachrichten sind im Debug-Logging sichtbar).

Geschäftlich sind das Nachrichten wie „neuer Deal“.

Eine wichtige Besonderheit ist, dass nach dem Start der Mikrodienst den Axon-Server fragen kann: „Was ist passiert?“, und der Server überträgt die gesammelten Ereignisse an den Mikrodienst. So kann der Mikrodienst relativ sicher neu gestartet werden, ohne dass Daten verloren gehen.
Bei diesem Austauschschema können wir sehr einfach viele Instanzen von Mikrodiensten starten,
und zwar auf unterschiedlichen Hosts.

Ja, eine Instanz des Axon-Servers ist nicht zuverlässig, aber vorerst ist es so.

Wir arbeiten in den Paradigmen Event Sourcing und CQRS. Das bedeutet, dass wir „Befehle“, „Ereignisse“ und „Abfragen“ haben müssen.

Wir werden einen Befehl haben: „Deal erstellen“, ein Ereignis „Deal erstellt“ und drei Abfragen: „alle Deals anzeigen“, „Position anzeigen“, „Position nach Instrument anzeigen“.

Das Arbeitschema sieht folgendermaßen aus:

  1. Der Mikrodienst tradeCreator empfängt einen Deal über Rest.
  2. Der Mikrodienst tradeCreator erstellt den Befehl „Deal erstellen“ und sendet ihn an den Axon-Server.
  3. Der Axon-Server empfängt den Befehl und leitet ihn an den interessierten Empfänger weiter, in unserem Fall ist das der Mikrodienst tradeCreator.
  4. Der Mikrodienst tradeCreator erhält den Befehl, formt das Ereignis „Deal erstellt“ und sendet es an den Axon-Server.
  5. Der Axon-Server nimmt das Ereignis an und leitet es an die interessierten Abonnenten weiter.
  6. Derzeit haben wir nur einen interessierten Empfänger – das ist der Mikrodienst tradeQueries.
  7. Der Mikrodienst tradeQueries empfängt das Ereignis und aktualisiert die internen Daten.

(Es ist wichtig, dass der Mikrodienst tradeQueries zum Zeitpunkt der Erstellung des Ereignisses möglicherweise nicht verfügbar ist, aber sobald er gestartet wird, erhält er sofort das Ereignis).

Ja, der Axon-Server steht im Zentrum der Kommunikation, alle Nachrichten gehen durch ihn.

Lass uns mit dem Codieren beginnen.

Um den Beitrag nicht mit Code zu überladen, werde ich unten nur Fragmente anführen, ein Link zum vollständigen Beispiel wird weiter unten sein.

Beginnen wir mit dem allgemeinen Modul common.

In ihm sind die allgemeinen Teile – es handelt sich um ein Ereignis (class CreatedTradeEvent). Beachte den Namen, es ist eigentlich der Name des Befehls, der dieses Ereignis ausgelöst hat, aber im vergangenen Zustand. Im vergangenen, da zuerst der Befehl auftritt, der zur Erstellung des Ereignisses führt.

Zu den anderen allgemeinen Strukturen gehören Klassen zur Beschreibung der Position (class Position), des Handels (class Trade) und der Handelsseite (enum Side), d.h. Kauf oder Verkauf.

Kommen wir zum Modul tradeCreator.

Dieses Modul hat eine Rest-Schnittstelle (class TradeController) zur Annahme von Handelsaufträgen.
Aus dem erhaltenen Handel wird der Befehl „Handel erstellen“ gebildet und an den Axon-Server gesendet.

    @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());
    }

Zur Verarbeitung des Befehls wird die Klasse class TradeAggregate verwendet.
Damit Axon ihn findet, fügen wir die Annotation @Aggregate hinzu.
Die Methode zur Verarbeitung des Befehls sieht so aus (verkürzt):

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

Aus dem Befehl wird ein Ereignis gebildet und an den Server gesendet.
Der Befehl befindet sich in der Klasse CreateTradeCommand.

Jetzt schauen wir uns das letzte Modul tradeQueries an.

Die Abfragen werden im Paket queries beschrieben.
In diesem Modul gibt es ebenfalls eine Rest-Schnittstelle.
public class TradeController.

Als Beispiel betrachten wir die Verarbeitung der Anfrage: „Alle Handelsaufträge anzeigen“.

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

Es wird eine Anfrage zur Abfrage erstellt und an den Server gesendet.

Zur Verarbeitung der Abfrage verwendet man die Klasse TradesEventHandler.
Diese enthält eine Methode, die mit der Annotation gekennzeichnet ist.

   @QueryHandler
    public List handleFindCurrentPositionQuery(FindCurrentPositionQuery query)

Diese ist verantwortlich für die Abfrage von Daten aus dem In-Memory-Speicher.

Die Frage stellt sich, wie in diesem Speicher die Informationen aktualisiert werden.

Beginnen wir damit, dass es sich einfach um eine Sammlung von ConcurrentHashMap handelt, die auf bestimmte Abfragen ausgelegt ist.
Zur Aktualisierung werden folgende Methoden verwendet:

    @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);
    }

Er empfängt das Ereignis „Handel erstellt“ und aktualisiert die Maps.

Das sind die wichtigsten Aspekte der Entwicklung von Mikrodiensten.

Was kann man über die Nachteile von Axon sagen?

Erstens, es wird eine komplexere Infrastruktur notwendig, und es gibt einen Single Point of Failure – den Axon-Server, alle Kommunikationen laufen über ihn.

Zweitens wird der Nachteil solcher verteilten Systeme deutlich – die vorübergehende Inkonsistenz der Daten. In unserem Fall kann zwischen dem Empfang eines neuen Handels und der Aktualisierung der Daten für die Abfragen unangemessen viel Zeit vergehen.

Was blieb im Hintergrund?

Es wurde nichts über Event Sourcing und CQRS gesagt, was es ist und wofür es benötigt wird.
Ohne die Klärung dieser Begriffe könnten einige Punkte unverständlich geblieben sein.

Es ist möglich, dass auch einzelne Codefragmente Erklärungen erfordern.

Darüber werden wir bei dem offenen Webinar am 21. September sprechen.

Ein vollständiges Beispiel.

Quelle: habr.com

60GB SSD 8Gb DDR4