Microservizi con comunicazione tramite Axon

In questo semplice tutorial creeremo un paio di microservizi su Spring Boot e organizzeremo l'interazione tra di essi tramite il framework Axon.

Microservizi con comunicazione tramite Axon


Supponiamo di avere un compito del genere.

Esiste una fonte di transazioni sul mercato azionario. Questa fonte ci trasmette le transazioni tramite un'interfaccia Rest.

Dobbiamo ottenere queste transazioni, conservarle nel database e creare un comodo archivio in memoria.

Questo archivio deve svolgere le seguenti funzioni:

  • restituire un elenco di operazioni;
  • restituire la posizione completa, ovvero una tabella "strumento" — "quantità attuale di titoli";
  • restituire la posizione per uno strumento specifico.

Come ci approcceremo alla soluzione di questo compito?

Secondo i principi della moda microservizi, dobbiamo suddividere il compito in microservizi costitutivi:

  • ottenere le transazioni tramite Rest;
  • salvare le transazioni nel database;
  • archivio in memoria per la rappresentazione dei dati sulla posizione.

Allora, nell'ambito di questo tutorial, realizziamo il primo e il terzo servizio, mentre il secondo lo rimandiamo alla seconda parte (scrivete nei commenti se vi interessa).

Dunque, abbiamo due microservizi.

Il primo riceve dati dall'esterno.

Il secondo elabora questi dati e risponde alle richieste in ingresso.

Chiaramente, vogliamo ottenere scalabilità orizzontale, aggiornamenti senza interruzioni e altri vantaggi dei microservizi.

Qual è, molto impegnativa, la sfida che ci aspetta?

In realtà, ce ne sono molte, ma ora parliamo di come i dati fluiranno tra questi microservizi. Possono anche essere interconnessi tramite Rest, si può utilizzare una coda, ci sono molte opzioni con i loro pro e contro.

Esaminiamo uno dei possibili approcci: l'interazione asincrona tramite il framework Axon.

Quali sono i vantaggi di questa soluzione?

Innanzitutto, l'interazione asincrona aumenta la flessibilità (sì, ci sono anche svantaggi, ma per ora parliamo solo dei vantaggi).

In secondo luogo, riceviamo direttamente funzionalità integrate Event Sourcing e CQRS.
In terzo luogo, Axon fornisce un'infrastruttura pronta all'uso, e dobbiamo concentrarci solo sullo sviluppo della logica di business.

Iniziamo.

Il progetto sarà basato su Gradle. Avrà tre moduli:

  • common. modulo con strutture dati comuni (non amiamo il copia e incolla);
  • tradeCreator. modulo con microservizio per ricevere transazioni tramite Rest;
  • tradeQueries. modulo con microservizio per visualizzare la posizione.

Prendiamo Spring Boot come base e colliamo il starter di Axon.

Axon funziona molto bene anche senza Spring, ma li useremo insieme.

Qui ci si deve fermare e dire due parole su Axon.

È un sistema client-server. C'è un server – è un'applicazione separata, la avvieremo in Docker.

E ci sono i client che si integrano nei microservizi.
La situazione è la seguente. Prima si avvia il server Axon (in Docker), poi i nostri microservizi.

All'avvio, i microservizi cercano il server e iniziano a interagire con esso. L'interazione può essere convenzionalmente suddivisa in due tipi: tecnica e commerciale.

Tecnica – è lo scambio di messaggi "sono vivo" (questi messaggi possono essere visti in modalità di registrazione debug).

Commerciale – è lo scambio di messaggi come "nuova transazione".

Una caratteristica importante è che, dopo l'avvio, il microservizio può chiedere al server Axon "cosa è successo" e il server trasmette al microservizio gli eventi accumulati. In questo modo, il microservizio può essere riavviato in relativa sicurezza senza perdita di dati.
Con questo schema di scambio, possiamo avviare molto facilmente molte istanze di microservizi,
anche su host diversi.

Sì, un'istanza del server Axon non è affidabile, ma al momento è così.

Lavoriamo secondo i paradigmi Event Sourcing e CQRS. Significa che dobbiamo avere "comandi", "eventi" e "query".

Avremo un comando: "creare una transazione", un evento "transazione creata" e tre query: "mostra tutte le transazioni", "mostra posizione", "mostra posizione per strumento".

Lo schema di lavoro è il seguente:

  1. Il microservizio tradeCreator riceve la transazione tramite Rest.
  2. Il microservizio tradeCreator crea il comando "creare una transazione" e lo invia al server Axon.
  3. Il server Axon riceve il comando e lo inoltra al destinatario interessato, in questo caso il microservizio tradeCreator.
  4. Il microservizio tradeCreator riceve il comando, crea l'evento "transazione creata" e lo invia al server Axon.
  5. Il server Axon riceve l'evento e lo inoltra agli abbonati interessati.
  6. Attualmente, abbiamo solo un destinatario interessato: il microservizio tradeQueries.
  7. Il microservizio tradeQueries riceve l'evento e aggiorna i dati interni.

(È importante notare che nel momento in cui viene creato l'evento, il microservizio tradeQueries potrebbe non essere disponibile, ma appena si avvia, riceverà immediatamente l'evento).

Sì, il server Axon è al centro delle comunicazioni, tutti i messaggi passano attraverso di esso.

Passiamo alla codifica.

Per non sovraccaricare il post con il codice, qui fornirò solo frammenti; il link all'esempio completo sarà qui sotto.

Iniziamo dal modulo generale common.

Le parti comuni in esso sono l'evento (class CreatedTradeEvent). Notate il nome, in effetti è il nome del comando che ha generato questo evento, ma al passato. Al passato, poiché prima appare il comando che porta alla creazione dell'evento.

Tra le altre strutture comuni ci sono le classi per descrivere la posizione (class Position), le transazioni (class Trade) e la parte della transazione (enum Side), ovvero acquisto o vendita.

Passiamo al modulo tradeCreator.

Questo modulo ha un'interfaccia Rest (class TradeController) per la ricezione delle transazioni.
Dalla transazione ricevuta viene formata il comando "crea transazione" e inviato al server 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());
    }

Per elaborare il comando viene utilizzata la classe class TradeAggregate.
Per farlo trovare ad Axon, abbiniamo l'annotazione @Aggregate.
Il metodo per elaborare il comando appare così (con abbreviazione):

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

Dal comando viene formata un evento e inviato al server.
Il comando si trova nella classe CreateTradeCommand.

Ora diamo un'occhiata all'ultimo modulo tradeQueries.

Le query sono descritte nel pacchetto queries.
In questo modulo c'è anche un'interfaccia Rest.
public class TradeController.

Per esempio, vediamo l'elaborazione della richiesta: "mostra tutte le transazioni".

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

Viene creato un comando per la selezione e inviato al server.

Per elaborare la richiesta di selezione viene utilizzata la classe TradesEventHandler.
In essa c'è un metodo contrassegnato con l'annotazione.

   @QueryHandler
    public List handleFindCurrentPositionQuery(FindCurrentPositionQuery query)

È proprio lui a occuparsi della selezione dei dati dallo storage in memoria.

Sorge la domanda su come venga aggiornata l'informazione in questo storage.

Iniziamo col dire che si tratta semplicemente di un insieme di ConcurrentHashMap, mirati a selezioni specifiche.
Per aggiornarli si applica il metodo:

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

Riceve l'evento "trade creato" e aggiorna le Map.

Questi sono i punti principali dello sviluppo di microservizi.

Cosa si può dire sugli svantaggi di Axon?

In primo luogo, c'è la complicazione dell'infrastruttura, è emerso un punto di guasto – Axon-server, tutte le comunicazioni passano attraverso di esso.

In secondo luogo, si manifesta chiaramente un difetto di tali sistemi distribuiti – la temporanea incoerenza dei dati. Nel nostro caso, tra la ricezione di una nuova transazione e l'aggiornamento dei dati per le selezioni può passare un tempo inaccettabile.

Cosa è rimasto fuori?

Non si è affatto parlato di Event Sourcing e CQRS, cosa siano e a cosa servano.
Senza chiarire questi concetti, alcuni aspetti potrebbero non essere stati chiari.

Potrebbe essere necessario chiarire anche alcune parti del codice.

Ne parleremo durante il webinar aperto 21 settembre.

Esempio completo.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster