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

Angenommen, wir haben folgende Aufgabe.
Es gibt eine Quelle für Geschäfte am Aktienmarkt. Diese Quelle überträgt uns Transaktionen über eine REST-Schnittstelle.
Wir müssen diese Transaktionen abrufen, in einer Datenbank speichern und ein benutzerfreundliches In-Memory-Speicher bereitstellen.
Dieses Speichersystem sollte folgende Funktionen erfüllen:
- eine Liste der Trades zurückgeben;
- die vollständige Position zurückgeben, d.h. eine Tabelle «Instrument» — «aktuelle Anzahl an Aktien»;
- die Position für ein bestimmtes Instrument zurückgeben.
Wie nähern wir uns der Lösung dieses Problems?
Nach den Geboten der Mikrodienstemode müssen wir die Aufgabe in die entsprechenden Mikrodienste aufteilen:
- das Abrufen eines Trades über REST;
- das Speichern des Trades in der Datenbank;
- In-Memory-Speicher zur Darstellung der Daten zur Position.
Lass uns im Rahmen dieses Tutorials den ersten und den dritten Dienst erstellen, den zweiten lassen wir für den zweiten Teil (schreibt 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 beantwortet die eingehenden Anfragen.
Wir möchten natürlich horizontale Skalierung, unterbrechungsfreies Update und die anderen Vorteile von Mikrodiensten erreichen.
Welche, durchaus komplexe, Aufgabe steht uns bevor?
Tatsächlich gibt es viele, aber lassen Sie uns jetzt darüber sprechen, wie die Daten zwischen diesen Mikrodiensten ausgetauscht werden. Es ist möglich, REST zu implementieren, eine Warteschlange einzurichten, oder viele andere Lösungen mit ihren eigenen Vor- und Nachteilen zu finden.
Betrachten wir einen möglichen Ansatz – asynchronen Austausch über das Axon-Framework..
Welche Vorteile hat diese Lösung?
Erstens erhöht der asynchrone Austausch die Flexibilität (ja, hier gibt es auch einen Nachteil, aber wir sprechen bisher nur über die Vorteile).
Zweitens erhalten wir sofort einsatzbereite Funktionen. Event Sourcing und CQRS.
Drittens bietet Axon eine fertige Infrastruktur, sodass wir uns ausschließlich auf die Entwicklung der Geschäftslogik konzentrieren können.
Legen wir los.
Unser Projekt wird auf Gradle basieren. Es wird drei Module geben:
- common. ein Modul mit gemeinsamen Datenstrukturen (wir mögen kein Copy-Paste);
- tradeCreator. ein Modul mit einem Mikrodienst zur Annahme von Transaktionen über REST;
- tradeQueries. ein Modul mit einem Mikrodienst zur Anzeige der Position.
Wir nehmen Spring Boot als Grundlage und fügen den Axon-Starter hinzu.
Axon funktioniert hervorragend auch ohne Spring, aber wir werden sie gemeinsam verwenden.
Hier sollten wir innehalten und ein paar Worte über Axon verlieren.
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 sich in die Mikrodienste integrieren.
So sieht das Bild aus: Zuerst wird der Axon-Server (in Docker) gestartet, danach unsere Mikrodienste.
Beim Start suchen die Mikrodienste den Server und beginnen mit ihm zu interagieren. Die Interaktion lässt sich grob in zwei Arten unterteilen: technisch und geschäftlich.
Technisch – das ist der Austausch von Nachrichten wie „Ich bin da“ (solche Nachrichten können im Debug-Logging-Modus gesehen werden).
Geschäftlich – das sind Nachrichten wie „neuer Deal“.
Ein wichtiger Aspekt 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 Daten zu verlieren.
Bei diesem Schema können wir sehr einfach viele Instanzen von Mikrodiensten starten,
und das auf verschiedenen Hosts.
Ja, eine Instanz des Axon-Servers – das ist nicht zuverlässig, aber momentan ist es so.
Wir arbeiten mit 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:
- Mikroservice tradeCreator nimmt den Deal über Rest entgegen.
- Mikroservice tradeCreator erstellt den Befehl „Deal erstellen“ und sendet ihn an den Axon-Server.
- Der Axon-Server empfängt den Befehl und leitet ihn an den interessierten Empfänger weiter, in unserem Fall den Mikroservice tradeCreator.
- Mikroservice tradeCreator erhält den Befehl, formuliert das Ereignis „Deal erstellt“ und sendet es an den Axon-Server.
- Der Axon-Server empfängt das Ereignis und leitet es an die interessierten Abonnenten weiter.
- Derzeit haben wir nur einen interessierten Empfänger – das ist der Mikroservice tradeQueries.
- Mikroservice tradeQueries erhält das Ereignis und aktualisiert die internen Daten.
(Es ist wichtig, dass zum Zeitpunkt der Ereigniserstellung der Mikroservice tradeQueries 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 laufen über ihn.
Lass uns mit dem Codieren beginnen.
Um den Beitrag nicht mit Code zu überlasten, werde ich unten nur Ausschnitte anführen; der Link zum vollständigen Beispiel wird weiter unten sein.
Beginnen wir mit dem gemeinsamen Modul common.
In ihm sind die gemeinsamen Teile – dieses Ereignis (class CreatedTradeEvent). Beachte die Benennung: Es handelt sich im Grunde um den Namen des Befehls, der dieses Ereignis ausgelöst hat, allerdings in der Vergangenheit. In der Vergangenheit, weil zuerst der Befehl erscheint, der zur Erstellung des Ereignisses führt.
Zu den anderen gemeinsamen Strukturen gehören die 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) zum Empfang von Aufträgen.
Aus dem erhaltenen Auftrag wird der Befehl „Handel erstellen“ formuliert 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());
}
Zum Verarbeiten des Befehls wird die Klasse class TradeAggregate verwendet.
Um Axon zu ermöglichen, sie zu finden, fügen wir die Annotation @Aggregate hinzu.
Die Methode zur Verarbeitung des Kommandos sieht folgendermaßen 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 Kommando wird ein Ereignis erstellt und an den Server gesendet.
Das Kommando 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 auch ein REST-Interface.
public class TradeController.
Zum Beispiel betrachten wir die Verarbeitung der Anfrage: „zeige alle Geschäfte“.
@GetMapping("/trade/all")
public List findAllTrades() {
return queryGateway.query(new FindAllTradesQuery(),
ResponseTypes.multipleInstancesOf(Trade.class)).join();
}
Es wird eine Abfrage für die Auswahl erstellt und an den Server gesendet.
Zur Verarbeitung der Abfrage wird die Klasse TradesEventHandler verwendet.
Darin gibt es eine Methode, die mit einer Annotation versehen ist.
@QueryHandler
public List handleFindCurrentPositionQuery(FindCurrentPositionQuery query)
Diese ist für die Auswahl der Daten aus dem In-Memory-Speicher verantwortlich.
Es stellt sich die Frage, wie die Informationen in diesem Speicher aktualisiert werden.
Zunächst einmal handelt es sich um eine Sammlung von ConcurrentHashMap, die auf bestimmte Abfragen optimiert sind.
Zur Aktualisierung wird folgende Methode 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 Hauptpunkte in der Entwicklung von Mikrodiensten.
Was kann man über die Nachteile von Axon sagen?
Erstens führt es zu einer Komplexität der Infrastruktur und schafft einen Single Point of Failure – den Axon-Server. Alle Kommunikation läuft darüber.
Zweitens zeigen sich deutliche Nachteile solcher verteilten Systeme – zeitliche Inkonsistenzen der Daten. In unserem Fall kann zwischen dem Erhalt eines neuen Handels und der Aktualisierung der Daten für Abfragen eine unzulässig lange Zeit vergehen.
Was blieb im Hintergrund?
Es wurde nichts über Event Sourcing und CQRS gesagt, was das ist und wozu es dient.
Ohne die Erläuterung dieser Konzepte könnten einige Punkte unklar geblieben sein.
Möglicherweise erfordern auch bestimmte Codefragmente eine Erklärung.
Darüber werden wir sprechen bei am 21. September.
.
Quelle: habr.com
