In questo semplice tutoriale creiamo un paio di microservizi con Spring Boot e organizziamo l'interazione tra di essi attraverso il framework Axon.

Supponiamo di avere il seguente compito.
Abbiamo una fonte di operazioni nel mercato azionario. Questa fonte ci trasmette le operazioni tramite un'interfaccia Rest.
Dobbiamo ottenere queste operazioni, salvarle in un database e creare uno storage in memoria conveniente.
Questo storage deve svolgere le seguenti funzioni:
- restituire un elenco di trade;
- restituire la posizione completa, ovvero la tabella "strumento" — "quantità attuale di titoli";
- restituire la posizione per uno strumento specifico.
Come ci approcceremo per risolvere questo compito?
Secondo i principi della moda microservizi, dobbiamo suddividere il compito in microservizi costitutivi:
- ottenere l'operazione tramite Rest;
- salvare l'operazione nel database;
- storage in memoria per rappresentare i dati della posizione.
Facciamo in questo tutorial il primo e il terzo servizio, mentre il secondo lo rimandiamo alla seconda parte (scrivi nei commenti se ti interessa).
Quindi, abbiamo due microservizi.
Il primo riceve dati dall'esterno.
Il secondo elabora questi dati e risponde alle richieste in arrivo.
Vogliamo certamente ottenere scalabilità orizzontale, aggiornamenti senza interruzioni e altri vantaggi dei microservizi.
Qual è la sfida, piuttosto complessa, che abbiamo davanti?
In realtà ce ne sono molte, ma adesso parliamo di come i dati circoleranno tra questi microservizi. Possiamo implementare Rest, utilizzare qualche coda, o inventare molte altre cose con i loro pro e contro.
Consideriamo uno dei possibili approcci: l'interazione asincrona tramite il framework Axon.
Quali sono i vantaggi di questa soluzione?
In primo luogo, l'interazione asincrona aumenta la flessibilità (sì, c'è anche uno svantaggio, ma per ora parliamo solo dei vantaggi).
In secondo luogo, otteniamo subito Event Sourcing e CQRS.
In terzo luogo, Axon fornisce un'infrastruttura pronta all'uso, permettendoci di concentrarci solo sulla 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 via Rest;
- tradeQueries. modulo con microservizio per visualizzare la posizione.
Prenderemo Spring Boot come base e collegheremo il starter di Axon.
Axon funziona ottimamente anche senza Spring, ma li utilizzeremo insieme.
Qui è necessario fermarsi e dire alcune parole su Axon.
È un sistema client-server. C'è un server – è un'applicazione separata, che avvieremo in Docker.
E ci sono i client che si integrano nei microservizi.
Ecco come si presenta il tutto. 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 divisa in due tipi: tecnica e commerciale.
Tecnica – è lo scambio di messaggi "ci sono" (questi messaggi possono essere visualizzati in modalità di registrazione debug).
Commerciale – è lo scambio di messaggi come "nuova transazione".
Una caratteristica importante, dopo l'avvio, un 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 modo relativamente sicuro senza perdita di dati.
Con questo schema di scambio, possiamo avviare facilmente molte istanze di microservizi,
anche su diversi host.
Sì, un'istanza del server Axon non è affidabile, ma al momento funziona così.
Lavoriamo secondo le paradigmi di Event Sourcing e CQRS. Questo 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 la posizione», «mostra la posizione per strumento».
La schema di lavoro è la seguente:
- Il microservizio tradeCreator riceve una transazione tramite Rest.
- Il microservizio tradeCreator crea il comando «creare una transazione» e lo invia al server Axon.
- Il server Axon riceve il comando e lo inoltra al destinatario interessato, in questo caso al microservizio tradeCreator.
- Il microservizio tradeCreator riceve il comando, genera l'evento «transazione creata» e lo invia al server Axon.
- Il server Axon riceve l'evento e lo inoltra agli abbonati interessati.
- Attualmente abbiamo solo un destinatario interessato: il microservizio tradeQueries.
- Il microservizio tradeQueries riceve l'evento e aggiorna i dati interni.
(È importante notare che al momento della generazione dell'evento, il microservizio tradeQueries potrebbe non essere disponibile, ma non appena verrà avviato, riceverà immediatamente l'evento).
Sì, il server Axon si trova al centro delle comunicazioni, tutti i messaggi passano attraverso di esso.
Passiamo al coding.
Per evitare di appesantire il post con il codice, di seguito riporterò solo i frammenti; il link all'esempio completo sarà qui sotto.
Iniziamo con il modulo comune.
In esso le parti comuni sono l'evento (class CreatedTradeEvent). Nota il nome; in effetti, è il nome del comando che ha generato questo evento, ma al passato. Al passato perché prima appare il comando che porta alla creazione dell'evento.
Altre strutture comuni includono le classi per descrivere la posizione (class Position), la transazione (class Trade) e la parte della transazione (enum Side), ossia acquisto o vendita.
Passiamo al modulo tradeCreator.
Questo modulo ha un'interfaccia Rest (class TradeController) per ricevere le transazioni.
Dalla transazione ricevuta si forma il comando "crea transazione" che viene 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 si usa la classe class TradeAggregate.
Affinché Axon lo trovi, aggiungiamo l'annotazione @Aggregate.
Il metodo per gestire il comando appare così (in forma ridotta):
@CommandHandler
public TradeAggregate(CreateTradeCommand command) {
log.info("command: {}", command);
var event = CreatedTradeEvent.builder()
.tradeId(command.tradeId())
....
.build();
AggregateLifecycle.apply(event);
}
Dall comando viene generato 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, esaminiamo la gestione della richiesta: "mostra tutte le operazioni".
@GetMapping("/trade/all")
public List findAllTrades() {
return queryGateway.query(new FindAllTradesQuery(),
ResponseTypes.multipleInstancesOf(Trade.class)).join();
}
Viene creato un query di selezione e inviato al server.
Per gestire la richiesta di selezione si usa la classe TradesEventHandler.
In essa c'è un metodo annotato con
@QueryHandler
public List handleFindCurrentPositionQuery(FindCurrentPositionQuery query)
È proprio lui che si occupa di estrarre i dati dallo storage in-memory.
Sorge la domanda, come viene aggiornata l'informazione in questo storage.
Iniziamo con il fatto che è semplicemente un insieme di ConcurrentHashMap, progettato per selezioni specifiche.
Per il loro aggiornamento 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);
}Gestisce l’evento "trattativa creata" e aggiorna le Mappe.
Ecco i punti principali nello sviluppo dei microservizi.
Cosa si può dire sugli svantaggi di Axon?
In primo luogo, c'è la complessità dell'infrastruttura, è emersa un punto di guasto - Axon Server, tutte le comunicazioni passano attraverso di esso.
In secondo luogo, si manifesta chiaramente un difetto di questi sistemi distribuiti - l'inconsistenza temporale dei dati. Nel nostro caso, tra il ricevimento della nuova transazione e l'aggiornamento dei dati per le query può passare un tempo inaccettabilmente lungo.
Cosa è rimasto in ombra?
Non è stato detto nulla su Event Sourcing e CQRS, cosa siano e a cosa servano.
Senza la spiegazione di questi concetti, alcuni aspetti potrebbero non essere chiari.
È possibile che alcuni frammenti di codice richiedano chiarimenti.
Su questo ne parleremo durante 21 settembre.
.
Fonte: habr.com
