Comunicação de microsserviço: um tutorial de integração do Spring com o Redis
Publicados: 2022-03-11A arquitetura de microsserviços é uma abordagem muito popular no design e implementação de aplicativos da Web altamente escaláveis. A comunicação dentro de um aplicativo monolítico entre componentes geralmente é baseada em chamadas de método ou função dentro do mesmo processo. Um aplicativo baseado em microsserviços, por outro lado, é um sistema distribuído executado em várias máquinas.
A comunicação entre esses microsserviços é importante para se ter um sistema estável e escalável. Existem várias maneiras de fazer isso. A comunicação baseada em mensagens é uma maneira de fazer isso de forma confiável.
Ao usar o sistema de mensagens, os componentes interagem entre si trocando mensagens de forma assíncrona. As mensagens são trocadas por meio de canais.
Quando o Serviço A deseja se comunicar com o Serviço B, em vez de enviá-lo diretamente, A o envia para um canal específico. Quando o Serviço B quer ler a mensagem, ele pega a mensagem de um determinado canal de mensagem.
Neste tutorial Spring Integration, você aprenderá como implementar o sistema de mensagens em um aplicativo Spring usando o Redis. Você será guiado por um aplicativo de exemplo em que um serviço está enviando eventos na fila e outro serviço está processando esses eventos um por um.
Integração Spring
O projeto Spring Integration estende a estrutura Spring para fornecer suporte para mensagens entre ou dentro de aplicativos baseados em Spring. Os componentes são conectados por meio do paradigma de mensagens. Componentes individuais podem não estar cientes de outros componentes no aplicativo.
Spring Integration fornece uma ampla seleção de mecanismos para comunicação com sistemas externos. Os adaptadores de canal são um desses mecanismos usados para integração unidirecional (enviar ou receber). E os gateways são usados para cenários de solicitação/resposta (entrada ou saída).
O Apache Camel é uma alternativa amplamente utilizada. A integração do Spring geralmente é preferida em serviços baseados em Spring existentes, pois faz parte do ecossistema Spring.
Redis
O Redis é um armazenamento de dados na memória extremamente rápido. Ele também pode persistir em um disco opcionalmente. Ele suporta diferentes estruturas de dados, como pares de valores-chave simples, conjuntos, filas, etc.
Usar o Redis como uma fila facilita muito o compartilhamento de dados entre componentes e o dimensionamento horizontal. Um produtor ou vários produtores podem enviar dados para a fila e um consumidor ou vários consumidores podem extrair os dados e processar o evento.
Vários consumidores não podem consumir o mesmo evento — isso garante que um evento seja processado uma vez.
Benefícios de usar o Redis como uma fila de mensagens:
- Execução paralela de tarefas discretas de forma não bloqueante
- Grande performance
- Estabilidade
- Fácil monitoramento e depuração
- Fácil implementação e uso
Regras:
- Adicionar uma tarefa à fila deve ser mais rápido do que processar a tarefa em si.
- Consumir tarefas deve ser mais rápido do que produzi-las (e se não, adicione mais consumidores).
Integração Spring com Redis
Veja a seguir a criação de um aplicativo de amostra para explicar como usar o Spring Integration com o Redis.
Digamos que você tenha um aplicativo que permite que os usuários publiquem postagens. E você deseja criar um recurso de acompanhamento. Outro requisito é que toda vez que alguém publicar uma postagem, todos os seguidores sejam notificados por meio de algum canal de comunicação (por exemplo, e-mail ou notificação push).
Uma maneira de implementar isso é enviar um e-mail para cada seguidor assim que o usuário publicar algo. Mas o que acontece quando o usuário tem 1.000 seguidores? E quando 1.000 usuários publicam algo em 10 segundos, cada um com 1.000 seguidores? Além disso, a postagem do editor aguardará até que todos os e-mails sejam enviados?
Os sistemas distribuídos resolvem esse problema.
Esse problema específico pode ser resolvido usando uma fila. O serviço A (o produtor), que é responsável pela publicação das postagens, fará apenas isso. Ele publicará uma postagem e enviará um evento com a lista de usuários que precisam receber um e-mail e a postagem em si. A lista de usuários pode ser buscada no serviço B, mas para simplificar este exemplo, vamos enviá-la do serviço A.
Esta é uma operação assíncrona. Isso significa que o serviço que está publicando não terá que esperar para enviar e-mails.
O serviço B (o consumidor) puxará o evento da fila e o processará. Dessa forma, poderíamos facilmente dimensionar nossos serviços, e poderíamos ter n
consumidores enviando e-mails (processando eventos).
Então vamos começar com uma implementação no serviço do produtor. As dependências necessárias são:
<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>
Essas três dependências do Maven são necessárias:
- Jedis é um cliente Redis.
- A dependência do Spring Data Redis facilita o uso do Redis em Java. Ele fornece conceitos Spring familiares, como uma classe de modelo para uso de API principal e acesso leve a dados no estilo de repositório.
- Spring Integration Redis fornece uma extensão do modelo de programação Spring para suportar os conhecidos padrões de integração empresarial.
Em seguida, precisamos configurar o cliente 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; } }
A anotação @Value
significa que o Spring injetará o valor definido nas propriedades do aplicativo no campo. Isso significa que os valores redis.host
e redis.port
devem ser definidos nas propriedades do aplicativo.
Agora, precisamos definir a mensagem que queremos enviar para a fila. Uma mensagem de exemplo simples pode ser assim:
@Getter @Setter @Builder public class PostPublishedEvent { private String postUrl; private String postTitle; private List<String> emails; }
Observação: o projeto Lombok (https://projectlombok.org/) fornece @Getter
, @Setter
, @Builder
e muitas outras anotações para evitar a confusão de código com getters, setters e outras coisas triviais. Você pode aprender mais sobre isso neste artigo da Toptal.
A própria mensagem será salva no formato JSON na fila. Sempre que um evento for publicado na fila, a mensagem será serializada para JSON. E ao consumir da fila, a mensagem será desserializada.
Com a mensagem definida, precisamos definir a própria fila. No Spring Integration, isso pode ser feito facilmente por meio de uma configuração .xml
. A configuração deve ser colocada dentro do 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>
Na configuração, você pode ver a parte “int-redis:queue-outbound-channel-adapter”. Suas propriedades são:
- id: O nome do bean do componente.
- channel:
MessageChannel
do qual este endpoint recebe mensagens. - connection-factory: Uma referência a um bean
RedisConnectionFactory
. - queue: o nome da lista Redis na qual a operação push baseada em fila é executada para enviar mensagens Redis. Este atributo é mutuamente exclusivo com queue-expression.
- queue-expression: Uma expressão SpEL para determinar o nome da lista Redis usando a mensagem recebida em tempo de execução como a variável
#root
. Este atributo é mutuamente exclusivo com a fila. - serializer: uma referência de bean
RedisSerializer
. Por padrão, é umJdkSerializationRedisSerializer
. No entanto, para cargas deString
, umStringRedisSerializer
é usado se uma referência de serializador não for fornecida. - extract-payload: especifique se esse endpoint deve enviar apenas a carga útil para a fila do Redis ou a mensagem inteira. Seu valor padrão é
true
. - left-push: Especifique se este endpoint deve usar push esquerdo (quando
true
) ou push direito (quandofalse
) para gravar mensagens na lista Redis. Se true, a lista Redis atua como uma fila FIFO quando usada com um adaptador de canal de entrada de fila Redis padrão. Defina comofalse
para usar com software que lê a lista com pop à esquerda ou para obter uma ordem de mensagem semelhante a uma pilha. Seu valor padrão étrue
.
A próxima etapa é definir o gateway, que é mencionado na configuração .xml
. Para um gateway, estamos usando a classe RedisChannelGateway
do pacote org.toptal.queue
.

StringRedisSerializer
é usado para serializar a mensagem antes de salvar no Redis. Também na configuração .xml
, definimos o gateway e configuramos o RedisChannelGateway
como um serviço de gateway. Isso significa que o bean RedisChannelGateway
pode ser injetado em outros beans. Definimos a propriedade default-request-channel
porque também é possível fornecer referências de canal por método usando a anotação @Gateway
. Definição de classe:
public interface RedisChannelGateway { void enqueue(PostPublishedEvent event); }
Para conectar essa configuração em nosso aplicativo, precisamos importá-la. Isso é implementado na classe SpringIntegrationConfig
.
@ImportResource("classpath:WEB-INF/event-queue-config.xml") @AutoConfigureAfter(RedisConfig.class) @Configuration public class SpringIntegrationConfig { }
A anotação @ImportResource
é usada para importar arquivos de configuração Spring .xml
para @Configuration
. E a anotação @AutoConfigureAfter
é usada para sugerir que uma configuração automática deve ser aplicada após outras classes de configuração automática especificadas.
Agora vamos criar um serviço e implementar o método que enqueue
eventos na fila do 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 agora, você pode enviar facilmente uma mensagem para a fila usando o método enqueue
de QueueService
.
As filas Redis são simplesmente listas com um ou mais produtores e consumidores. Para publicar uma mensagem em uma fila, os produtores usam o comando LPUSH
Redis. E se você monitorar o Redis (dica: digite redis-cli monitor
), você pode ver que a mensagem é adicionada à fila:
"LPUSH" "my-event-queue" "{\"postUrl\":\"test\",\"postTitle\":\"test\",\"emails\":[\"test\"]}"
Agora, precisamos criar um aplicativo consumidor que irá puxar esses eventos da fila e processá-los. O serviço do consumidor precisa das mesmas dependências que o serviço do produtor.
Agora podemos reutilizar a classe PostPublishedEvent
para desserializar mensagens.
Precisamos criar a configuração da fila e, novamente, ela deve ser colocada dentro do resources/WEB-INF
. O conteúdo da configuração da fila é:
<?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>
Na configuração .xml
, int-redis:queue-inbound-channel-adapter
pode ter as seguintes propriedades:
- id: O nome do bean do componente.
- channel: O
MessageChannel
para o qual enviamos mensagens deste endpoint. - inicialização automática: Um atributo
SmartLifecycle
para especificar se esse terminal deve iniciar automaticamente após o início do contexto do aplicativo ou não. Seu valor padrão étrue
. - phase: Um atributo
SmartLifecycle
para especificar a fase na qual este terminal será iniciado. Seu valor padrão é0
. - connection-factory: Uma referência a um bean
RedisConnectionFactory
. - queue: o nome da lista Redis na qual a operação pop baseada em fila é executada para obter mensagens Redis.
- error-channel: O
MessageChannel
para o qual enviaremosErrorMessages
comExceptions
da tarefa de escuta doEndpoint
. Por padrão, oMessagePublishingErrorHandler
subjacente usa oerrorChannel
padrão do contexto do aplicativo. - serializer: A referência do bean
RedisSerializer
. Pode ser uma string vazia, o que significa que não há serializador. Nesse caso, obyte[]
da mensagem Redis de entrada é enviado ao canal como a carga útil daMessage
. Por padrão, é umJdkSerializationRedisSerializer
. - receive-timeout: o tempo limite em milissegundos para a operação pop aguardar uma mensagem Redis da fila. Seu valor padrão é 1 segundo.
- intervalo de recuperação: o tempo em milissegundos durante o qual a tarefa de ouvinte deve dormir após exceções na operação pop antes de reiniciar a tarefa de ouvinte.
- expect-message: especifique se este endpoint espera que os dados da fila do Redis contenham mensagens inteiras. Se esse atributo for definido como
true
, o serializador não poderá ser uma string vazia porque as mensagens exigem alguma forma de desserialização (serialização JDK por padrão). Seu valor padrão éfalse
. - task-executor: Uma referência a um bean Spring
TaskExecutor
(ou JDK 1.5+ Executor padrão). Ele é usado para a tarefa de escuta subjacente. Por padrão, umSimpleAsyncTaskExecutor
é usado. - right-pop: especifique se esse endpoint deve usar pop à direita (quando
true
) ou pop à esquerda (quandofalse
) para ler mensagens da lista Redis. Setrue
, a lista Redis atua como uma fila FIFO quando usada com um adaptador de canal de saída de fila Redis padrão. Defina comofalse
para usar com software que grava na lista com o botão direito ou para obter uma ordem de mensagem semelhante à pilha. Seu valor padrão étrue
.
A parte importante é o “ativador de serviço”, que define qual serviço e método deve ser usado para processar o evento.'
Além disso, o json-to-object-transformer
precisa de um atributo type para transformar JSON em objetos, definido acima como type="com.toptal.integration.spring.model.PostPublishedEvent"
.
Novamente, para conectar essa configuração, precisaremos da classe SpringIntegrationConfig
, que pode ser a mesma de antes. E por último, precisamos de um serviço que realmente processe o 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 :) } }
Depois de executar o aplicativo, você pode ver no Redis:
"BRPOP" "my-event-queue" "1"
Conclusão
Com Spring Integration e Redis, construir um aplicativo de microsserviços Spring não é tão assustador quanto normalmente seria. Com um pouco de configuração e uma pequena quantidade de código clichê, você pode construir as bases de sua arquitetura de microsserviços rapidamente.
Mesmo que você não planeje riscar totalmente seu projeto Spring atual e mudar para uma nova arquitetura, com a ajuda do Redis, é muito simples obter grandes melhorias de desempenho com filas.