Comunicazione di microservizi: un tutorial di integrazione primaverile con Redis

Pubblicato: 2022-03-11

L'architettura di microservizi è un approccio molto diffuso nella progettazione e nell'implementazione di applicazioni Web altamente scalabili. La comunicazione all'interno di un'applicazione monolitica tra i componenti si basa generalmente su chiamate di metodi o funzioni all'interno dello stesso processo. Un'applicazione basata su microservizi, invece, è un sistema distribuito in esecuzione su più macchine.

La comunicazione tra questi microservizi è importante per avere un sistema stabile e scalabile. Ci sono diversi modi per farlo. La comunicazione basata su messaggi è un modo per farlo in modo affidabile.

Quando si utilizza la messaggistica, i componenti interagiscono tra loro scambiandosi messaggi in modo asincrono. I messaggi vengono scambiati attraverso i canali.

rappresentazione grafica di un sistema di messaggistica che facilita la comunicazione tra il servizio A e il servizio B

Quando il servizio A vuole comunicare con il servizio B, invece di inviarlo direttamente, A lo invia a un canale specifico. Quando il servizio B vuole leggere il messaggio, preleva il messaggio da un particolare canale di messaggi.

In questo tutorial sull'integrazione di Spring, imparerai come implementare la messaggistica in un'applicazione Spring utilizzando Redis. Verrà visualizzata un'applicazione di esempio in cui un servizio sta inviando gli eventi nella coda e un altro servizio sta elaborando questi eventi uno per uno.

Integrazione primaverile

Il progetto Spring Integration estende il framework Spring per fornire supporto per la messaggistica tra o all'interno di applicazioni basate su Spring. I componenti sono collegati tra loro tramite il paradigma della messaggistica. I singoli componenti potrebbero non essere a conoscenza di altri componenti nell'applicazione.

Spring Integration fornisce un'ampia selezione di meccanismi per comunicare con i sistemi esterni. Gli adattatori di canale sono uno di questi meccanismi utilizzati per l'integrazione unidirezionale (invio o ricezione). E i gateway vengono utilizzati per scenari di richiesta/risposta (in entrata o in uscita).

Apache Camel è un'alternativa ampiamente utilizzata. L'integrazione primaverile è solitamente preferita nei servizi esistenti basati su Spring poiché fa parte dell'ecosistema Spring.

Redis

Redis è un datastore in memoria estremamente veloce. Facoltativamente può persistere anche su un disco. Supporta diverse strutture di dati come semplici coppie chiave-valore, insiemi, code, ecc.

L'utilizzo di Redis come coda semplifica notevolmente la condivisione dei dati tra i componenti e il ridimensionamento orizzontale. Uno o più produttori possono inviare i dati alla coda e uno o più consumatori possono estrarre i dati ed elaborare l'evento.

Più consumer non possono consumare lo stesso evento: ciò garantisce che un evento venga elaborato una volta.

diagramma che mostra l'architettura produttore/consumatore

Vantaggi dell'utilizzo di Redis come coda di messaggi:

  • Esecuzione parallela di attività discrete in modo non bloccante
  • Grande esibizione
  • Stabilità
  • Facile monitoraggio e debug
  • Facile implementazione e utilizzo

Regole:

  • L'aggiunta di un'attività alla coda dovrebbe essere più veloce dell'elaborazione dell'attività stessa.
  • Il consumo delle attività dovrebbe essere più veloce della produzione (e in caso contrario, aggiungere più consumatori).

Integrazione primaverile con Redis

Di seguito viene illustrata la creazione di un'applicazione di esempio per spiegare come utilizzare Spring Integration con Redis.

Supponiamo che tu abbia un'applicazione che consente agli utenti di pubblicare post. E vuoi creare una funzione di follow. Un altro requisito è che ogni volta che qualcuno pubblica un post, tutti i follower dovrebbero essere avvisati tramite qualche canale di comunicazione (ad esempio, e-mail o notifica push).

Un modo per implementarlo è inviare un'e-mail a ciascun follower una volta che l'utente pubblica qualcosa. Ma cosa succede quando l'utente ha 1.000 follower? E quando 1.000 utenti pubblicano qualcosa in 10 secondi, ognuno dei quali ha 1.000 follower? Inoltre, il post dell'editore attenderà l'invio di tutte le email?

I sistemi distribuiti risolvono questo problema.

Questo problema specifico può essere risolto utilizzando una coda. Il servizio A (il produttore), che è responsabile della pubblicazione dei post, lo farà. Pubblicherà un post e spingerà un evento con l'elenco degli utenti che devono ricevere un'e-mail e il post stesso. L'elenco degli utenti potrebbe essere recuperato nel servizio B, ma per semplicità di questo esempio lo invieremo dal servizio A.

Questa è un'operazione asincrona. Ciò significa che il servizio che sta pubblicando non dovrà attendere per inviare e-mail.

Il servizio B (il consumatore) estrarrà l'evento dalla coda e lo elaborerà. In questo modo, potremmo facilmente ridimensionare i nostri servizi e potremmo avere n consumatori che inviano e-mail (eventi di elaborazione).

Quindi iniziamo con un'implementazione al servizio del produttore. Le dipendenze necessarie sono:

 <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-redis</artifactId> </dependency>

Queste tre dipendenze Maven sono necessarie:

  • Jedis è un cliente Redis.
  • La dipendenza Spring Data Redis semplifica l'utilizzo di Redis in Java. Fornisce concetti Spring familiari come una classe modello per l'utilizzo dell'API di base e un accesso ai dati in stile repository leggero.
  • Spring Integration Redis fornisce un'estensione del modello di programmazione Spring per supportare i noti modelli di integrazione aziendale.

Successivamente, dobbiamo configurare il client Jedis:

 @Configuration public class RedisConfig { @Value("${redis.host}") private String redisHost; @Value("${redis.port:6379}") private int redisPort; @Bean public JedisPoolConfig poolConfig() { JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(128); return poolConfig; } @Bean public RedisConnectionFactory redisConnectionFactory(JedisPoolConfig poolConfig) { final JedisConnectionFactory connectionFactory = new JedisConnectionFactory(); connectionFactory.setHostName(redisHost); connectionFactory.setPort(redisPort); connectionFactory.setPoolConfig(poolConfig); connectionFactory.setUsePool(true); return connectionFactory; } }

L'annotazione @Value significa che Spring inietterà il valore definito nelle proprietà dell'applicazione nel campo. Ciò significa che i valori redis.host e redis.port devono essere definiti nelle proprietà dell'applicazione.

Ora dobbiamo definire il messaggio che vogliamo inviare alla coda. Un semplice messaggio di esempio potrebbe essere simile a:

 @Getter @Setter @Builder public class PostPublishedEvent { private String postUrl; private String postTitle; private List<String> emails; }

Nota: Project Lombok (https://projectlombok.org/) fornisce @Getter , @Setter , @Builder e molte altre annotazioni per evitare di ingombrare il codice con getter, setter e altre cose banali. Puoi saperne di più su questo articolo di Toptal.

Il messaggio stesso verrà salvato in formato JSON nella coda. Ogni volta che un evento viene pubblicato nella coda, il messaggio verrà serializzato in JSON. E quando si consuma dalla coda, il messaggio verrà deserializzato.

Con il messaggio definito, dobbiamo definire la coda stessa. In Spring Integration, può essere fatto facilmente tramite una configurazione .xml . La configurazione deve essere inserita nella directory resources/WEB-INF .

 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:int="http://www.springframework.org/schema/integration" xmlns:int-redis="http://www.springframework.org/schema/integration/redis" xsi:schemaLocation="http://www.springframework.org/schema/integration/redis http://www.springframework.org/schema/integration/redis/spring-integration-redis.xsd http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <int-redis:queue-outbound-channel-adapter channel="eventChannelJson" serializer="serializer" auto-startup="true" connection-factory="redisConnectionFactory" queue="my-event-queue" /> <int:gateway service-interface="org.toptal.queue.RedisChannelGateway" error-channel="errorChannel" default-request-channel="eventChannel"> <int:default-header name="topic" value="queue"/> </int:gateway> <int:channel/> <int:channel/> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> <int:object-to-json-transformer input-channel="eventChannel" output-channel="eventChannelJson"/> </beans>

Nella configurazione, puoi vedere la parte "int-redis:queue-outbound-channel-adapter". Le sue proprietà sono:

  • id: il nome del bean del componente.
  • canale: MessageChannel da cui questo endpoint riceve i messaggi.
  • connection-factory: un riferimento a un bean RedisConnectionFactory .
  • coda: il nome dell'elenco Redis su cui viene eseguita l'operazione push basata sulla coda per inviare messaggi Redis. Questo attributo si esclude a vicenda con queue-expression.
  • queue-expression: un'espressione SpEL per determinare il nome dell'elenco Redis utilizzando il messaggio in arrivo in fase di esecuzione come variabile #root . Questo attributo si esclude a vicenda con la coda.
  • serializer: un riferimento al bean RedisSerializer . Per impostazione predefinita, è un JdkSerializationRedisSerializer . Tuttavia, per i payload String , viene usato un StringRedisSerializer se non viene fornito un riferimento al serializzatore.
  • extract-payload: specifica se questo endpoint deve inviare solo il payload alla coda Redis o l'intero messaggio. Il suo valore predefinito è true .
  • left-push: specifica se questo endpoint deve utilizzare il push sinistro (quando true ) o il push destro (quando false ) per scrivere messaggi nell'elenco Redis. Se true, l'elenco Redis funge da coda FIFO quando viene utilizzato con un adattatore del canale in ingresso della coda Redis predefinito. Impostare su false per l'uso con software che legge dall'elenco con il pop sinistro o per ottenere un ordine dei messaggi simile a uno stack. Il suo valore predefinito è true .

Il passaggio successivo consiste nel definire il gateway, menzionato nella configurazione .xml . Per un gateway, utilizziamo la classe RedisChannelGateway dal pacchetto org.toptal.queue .

StringRedisSerializer viene utilizzato per serializzare il messaggio prima del salvataggio in Redis. Anche nella configurazione .xml , abbiamo definito il gateway e impostato RedisChannelGateway come servizio gateway. Ciò significa che il bean RedisChannelGateway potrebbe essere iniettato in altri bean. Abbiamo definito la proprietà default-request-channel perché è anche possibile fornire riferimenti al canale per metodo utilizzando l'annotazione @Gateway . Definizione di classe:

 public interface RedisChannelGateway { void enqueue(PostPublishedEvent event); }

Per cablare questa configurazione nella nostra applicazione, dobbiamo importarla. Questo è implementato nella classe SpringIntegrationConfig .

 @ImportResource("classpath:WEB-INF/event-queue-config.xml") @AutoConfigureAfter(RedisConfig.class) @Configuration public class SpringIntegrationConfig { }

L'annotazione @ImportResource viene utilizzata per importare i file di configurazione Spring .xml in @Configuration . E l'annotazione @AutoConfigureAfter viene utilizzata per suggerire che una configurazione automatica deve essere applicata dopo altre classi di configurazione automatica specificate.

Ora creeremo un servizio e implementeremo il metodo che enqueue gli eventi alla coda Redis.

 public interface QueueService { void enqueue(PostPublishedEvent event); }
 @Service public class RedisQueueService implements QueueService { private RedisChannelGateway channelGateway; @Autowired public RedisQueueService(RedisChannelGateway channelGateway) { this.channelGateway = channelGateway; } @Override public void enqueue(PostPublishedEvent event) { channelGateway.enqueue(event); } }

E ora puoi inviare facilmente un messaggio alla coda utilizzando il metodo di enqueue da QueueService .

Le code Redis sono semplicemente elenchi con uno o più produttori e consumatori. Per pubblicare un messaggio in una coda, i produttori utilizzano il comando LPUSH Redis. E se monitori Redis (suggerimento: digita redis-cli monitor ), puoi vedere che il messaggio viene aggiunto alla coda:

 "LPUSH" "my-event-queue" "{\"postUrl\":\"test\",\"postTitle\":\"test\",\"emails\":[\"test\"]}"

Ora, dobbiamo creare un'applicazione consumer che estragga questi eventi dalla coda e li elabori. Il servizio consumatore necessita delle stesse dipendenze del servizio produttore.

Ora possiamo riutilizzare la classe PostPublishedEvent per deserializzare i messaggi.

Dobbiamo creare la configurazione della coda e, ancora, deve essere collocata all'interno della directory resources/WEB-INF . Il contenuto della configurazione della coda è:

 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:int="http://www.springframework.org/schema/integration" xmlns:int-redis="http://www.springframework.org/schema/integration/redis" xsi:schemaLocation="http://www.springframework.org/schema/integration/redis http://www.springframework.org/schema/integration/redis/spring-integration-redis.xsd http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <int-redis:queue-inbound-channel-adapter channel="eventChannelJson" queue="my-event-queue" serializer="serializer" auto-startup="true" connection-factory="redisConnectionFactory"/> <int:channel/> <int:channel> <int:queue/> </int:channel> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> <int:json-to-object-transformer input-channel="eventChannelJson" output-channel="eventChannel" type="com.toptal.integration.spring.model.PostPublishedEvent"/> <int:service-activator input-channel="eventChannel" ref="RedisEventProcessingService" method="process"> <int:poller fixed-delay="10" time-unit="SECONDS" max-messages-per-poll="500"/> </int:service-activator> </beans>

Nella configurazione .xml , int-redis:queue-inbound-channel-adapter può avere le seguenti proprietà:

  • id: il nome del bean del componente.
  • canale: il MessageChannel a cui inviamo i messaggi da questo endpoint.
  • avvio automatico: un attributo SmartLifecycle per specificare se questo endpoint deve avviarsi automaticamente dopo l'avvio del contesto dell'applicazione o meno. Il suo valore predefinito è true .
  • fase: un attributo SmartLifecycle per specificare la fase in cui verrà avviato questo endpoint. Il suo valore predefinito è 0 .
  • connection-factory: un riferimento a un bean RedisConnectionFactory .
  • coda: il nome dell'elenco Redis su cui viene eseguita l'operazione pop basata sulla coda per ottenere i messaggi Redis.
  • error-channel: il MessageChannel a cui invieremo ErrorMessages con Exceptions dall'attività di ascolto Endpoint . Per impostazione predefinita, il MessagePublishingErrorHandler sottostante usa l' errorChannel predefinito dal contesto dell'applicazione.
  • serializer: il riferimento al bean RedisSerializer . Può essere una stringa vuota, il che significa nessun serializzatore. In questo caso, il byte[] dal messaggio Redis in entrata viene inviato al canale come payload del Message . Per impostazione predefinita, è un JdkSerializationRedisSerializer .
  • ricezione-timeout: il timeout in millisecondi per l'operazione pop per attendere un messaggio Redis dalla coda. Il suo valore predefinito è 1 secondo.
  • recovery-interval: il tempo in millisecondi per il quale l'attività del listener dovrebbe dormire dopo le eccezioni nell'operazione pop prima di riavviare l'attività del listener.
  • Expect-message: specificare se questo endpoint prevede che i dati dalla coda Redis contengano interi messaggi. Se questo attributo è impostato su true , il serializzatore non può essere una stringa vuota perché i messaggi richiedono una qualche forma di deserializzazione (serializzazione JDK per impostazione predefinita). Il suo valore predefinito è false .
  • task-executor: un riferimento a un bean Spring TaskExecutor (o standard JDK 1.5+ Executor). Viene utilizzato per l'attività di ascolto sottostante. Per impostazione predefinita, viene utilizzato un SimpleAsyncTaskExecutor .
  • right-pop: specifica se questo endpoint deve utilizzare il pop destro (quando true ) o il pop sinistro (quando false ) per leggere i messaggi dall'elenco Redis. Se true , l'elenco Redis funge da coda FIFO quando viene utilizzato con un adattatore del canale in uscita della coda Redis predefinito. Impostato su false per l'uso con il software che scrive nell'elenco con la giusta pressione o per ottenere un ordine di messaggi simile a uno stack. Il suo valore predefinito è true .

La parte importante è "l'attivatore del servizio", che definisce quale servizio e metodo dovrebbe essere utilizzato per elaborare l'evento.'

Inoltre, json-to-object-transformer necessita di un attributo type per trasformare JSON in oggetti, impostato sopra su type="com.toptal.integration.spring.model.PostPublishedEvent" .

Ancora una volta, per cablare questa configurazione, avremo bisogno della classe SpringIntegrationConfig , che può essere la stessa di prima. E infine, abbiamo bisogno di un servizio che elabori effettivamente l'evento.

 public interface EventProcessingService { void process(PostPublishedEvent event); } @Service("RedisEventProcessingService") public class RedisEventProcessingService implements EventProcessingService { @Override public void process(PostPublishedEvent event) { // TODO: Send emails here, retry strategy, etc :) } }

Una volta eseguita l'applicazione, in Redis puoi vedere:

 "BRPOP" "my-event-queue" "1"

Conclusione

Con Spring Integration e Redis, la creazione di un'applicazione di microservizi Spring non è così scoraggiante come lo sarebbe normalmente. Con una piccola configurazione e una piccola quantità di codice standard, puoi creare le basi della tua architettura di microservizi in pochissimo tempo.

Anche se non hai intenzione di grattare completamente il tuo attuale progetto Spring e passare a una nuova architettura, con l'aiuto di Redis, è molto semplice ottenere enormi miglioramenti delle prestazioni con le code.