Microservices avec communication via Axon

Dans ce tutoriel simple, nous allons créer quelques microservices avec Spring Boot et organiser leur interaction via le cadre Axon.

Microservices avec communication via Axon


Supposons que nous avons la tâche suivante.

Il existe une source d'opérations sur le marché boursier. Cette source nous transmet des opérations via une interface Rest.

Nous devons récupérer ces opérations, les enregistrer dans une base de données et créer un stockage in-memory pratique.

Ce stockage doit remplir les fonctions suivantes :

  • retourner une liste de trades;
  • retourner la position complète, c'est-à-dire le tableau « instrument » — « quantité actuelle de titres »;
  • retourner la position pour un instrument donné.

Comment allons-nous aborder la solution de cette tâche ?

Selon les préceptes de la mode des microservices, nous devons diviser la tâche en microservices constituants :

  • récupération des transactions via Rest;
  • enregistrement des transactions dans la base de données;
  • stockage in-memory pour la représentation des données de la position.

Faisons dans ce tutoriel le premier et le troisième service, en laissant le second pour la deuxième partie (faites-le nous savoir dans les commentaires si cela vous intéresse).

Ainsi, nous avons deux microservices.

Le premier obtient des données de l'extérieur.

Le second traite ces données et répond aux requêtes entrantes.

Nous voulons bien sûr obtenir une scalabilité horizontale, une mise à jour sans interruption et d'autres avantages des microservices.

Quelle tâche, assez complexe, nous attend ?

En réalité, il y en a beaucoup, mais parlons maintenant de la manière dont les données vont circuler entre ces microservices. On peut aussi faire un Rest entre eux, installer une sorte de queue, ou imaginer beaucoup d'autres choses avec leurs avantages et inconvénients.

Examinons une des approches possibles – l'interaction asynchrone via le cadre Axon..

Quels sont les avantages de cette solution ?

Tout d'abord, l'interaction asynchrone augmente la flexibilité (oui, il y a aussi un inconvénient ici, mais nous parlons pour l'instant uniquement des avantages).

Deuxièmement, nous obtenons directement de l'ordre du sujet. Event Sourcing et CQRS.
Troisièmement, Axon fournit une infrastructure prête à l'emploi et nous devons nous concentrer uniquement sur le développement de la logique métier.

Commençons.

Notre projet sera sous gradle. Il comportera trois modules :

  • common. module avec des structures de données communes (nous n'aimons pas le copié-collé);
  • tradeCreator. module avec un microservice pour la réception des transactions via Rest;
  • tradeQueries. module avec un microservice pour l'affichage de la position.

Prenons Spring Boot comme base et ajoutons le starter d'Axon.

Axon fonctionne très bien même sans Spring, mais nous allons les utiliser ensemble.

Ici, il faut s'arrêter un instant et dire quelques mots sur Axon.

C'est un système client-serveur. Il y a un serveur - c'est une application distincte, que nous allons exécuter dans Docker.

Et il y a des clients qui s'intègrent dans des microservices.
Cela donne un aperçu général. D'abord, le serveur Axon (dans Docker) est démarré, puis nos microservices.

Au démarrage, les microservices recherchent le serveur et commencent à interagir avec lui. L'interaction peut être grossièrement divisée en deux types : technique et commerciale.

Technique - c'est l'échange de messages 'je suis en vie' (ces messages peuvent être vus en mode de journalisation debug).

Commerciale - c'est l'échange de messages comme 'nouvelle transaction'.

Une caractéristique importante, après le démarrage, le microservice peut demander au serveur Axon 'que s'est-il passé' et le serveur transmet les événements accumulés au microservice. De cette manière, le microservice peut être redémarré relativement en toute sécurité sans perdre de données.
Avec ce système d'échange, nous pouvons facilement lancer de nombreux exemplaires de microservices,
et ce, sur des hôtes différents.

Oui, un seul exemplaire d'Axon-serveur n'est pas fiable, mais pour l'instant, c'est comme ça.

Nous travaillons dans les paradigmes Event Sourcing et CQRS. Cela signifie que nous devons avoir des 'commandes', des 'événements' et des 'projections'.

Nous aurons une commande : 'créer une transaction', un événement 'transaction créée' et trois projections : 'montrer toutes les transactions', 'montrer la position', 'montrer la position par instrument'.

Le schéma de travail est le suivant :

  1. Le microservice tradeCreator reçoit une transaction par Rest.
  2. Le microservice tradeCreator crée une commande 'créer une transaction' et l'envoie au serveur Axon.
  3. Le serveur Axon reçoit la commande et la transmet au destinataire concerné, dans notre cas, c'est le microservice tradeCreator.
  4. Le microservice tradeCreator reçoit la commande, forme l'événement 'transaction créée' et l'envoie au serveur Axon.
  5. Le serveur Axon reçoit l'événement et le transmet aux abonnés intéressés.
  6. Actuellement, nous n'avons qu'un seul destinataire intéressé - c'est le microservice tradeQueries.
  7. Le microservice tradeQueries reçoit l'événement et met à jour ses données internes.

(Il est important que, au moment de la création de l'événement, le microservice tradeQueries puisse ne pas être disponible, mais dès qu'il sera démarré, il recevra immédiatement l'événement).

Oui, le serveur Axon est au centre des communications, tous les messages passent par lui.

Passons au codage.

Pour ne pas surcharger le post avec du code, je vais fournir ci-dessous seulement des fragments, le lien vers l'exemple complet sera plus bas.

Commençons par le module général common.

Les parties communes comprennent l'événement (class CreatedTradeEvent). Notez la nomination, c'est en fait le nom de la commande qui a généré cet événement, mais au passé. Au passé, car d'abord la commande apparaît, entraînant la création de l'événement.

Les autres structures communes incluent des classes pour décrire la position (class Position), la transaction (class Trade) et le côté de la transaction (enum Side), c'est-à-dire achat ou vente.

Passons au module tradeCreator.

Ce module dispose d'une interface Rest (class TradeController) pour recevoir des transactions.
La transaction reçue est transformée en commande « créer une transaction » et envoyée au serveur 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());
    }

La commande est traitée par la classe class TradeAggregate.
Pour qu'Axon le trouve, nous mettons l'annotation @Aggregate.
La méthode pour traiter la commande ressemble à ceci (avec un raccourci) :

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

De la commande, un événement est généré et envoyé au serveur.
La commande se trouve dans la classe CreateTradeCommand.

Voyons maintenant le dernier module tradeQueries.

Les requêtes sont décrites dans le paquet queries.
Ce module dispose également d'une interface Rest
public class TradeController.

Pour exemple, regardons le traitement de la requête : « montrer toutes les transactions ».

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

Une requête de sélection est créée et envoyée au serveur.

La requête de sélection est traitée par la classe TradesEventHandler.
Il y a une méthode marquée avec l'annotation

   @QueryHandler
    public List handleFindCurrentPositionQuery(FindCurrentPositionQuery query)

C'est cette méthode qui est responsable de la sélection des données de la mémoire.

La question se pose, comment les informations sont-elles mises à jour dans cette mémoire ?

Commençons par le fait que c'est simplement un ensemble de ConcurrentHashMap, adaptées à des sélections spécifiques.
Pour leur mise à jour, la méthode suivante est utilisée :

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

Il reçoit l'événement « transaction créée » et met à jour les Maps.

Ce sont les points clés du développement des microservices.

Que peut-on dire sur les inconvénients d'Axon ?

Tout d'abord, cela complique l'infrastructure, une nouvelle point de défaillance apparaît - le serveur Axon, toutes les communications passent par lui.

Ensuite, on voit très clairement le problème des systèmes distribués similaires - l'incohérence temporelle des données. Dans notre cas, l'obtention d'une nouvelle transaction et la mise à jour des données pour les sélections peuvent prendre un temps inacceptable.

Qu'est-ce qui reste en dehors du cadre ?

Rien n'est dit sur l'Event Sourcing et le CQRS, ce que c'est et pourquoi c'est nécessaire.
Sans expliquer ces concepts, certains points pourraient rester flous.

Peut-être que certains fragments de code nécessitent également des explications.

Nous en parlerons lors de un webinaire ouvert le 21 septembre.

Un exemple complet.

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster