Introdução ao Lettuce - o cliente Java Redis

Introdução ao Lettuce - o cliente Java Redis

1. Visão geral

Este artigo é uma introdução aLettuce, um cliente JavaRedis.

O Redis é um armazenamento de valores-chave na memória que pode ser usado como banco de dados, cache ou intermediário de mensagens. Os dados são adicionados, consultados, modificados e excluídos comcommands que operam em chaves na estrutura de dados na memória do Redis.

O Lettuce suporta o uso de comunicação síncrona e assíncrona da API Redis completa, incluindo suas estruturas de dados, mensagens pub / sub e conexões de servidor de alta disponibilidade.

2. Por que alface?

Abordamos Jedisin one of the previous posts. O que torna a alface diferente?

A diferença mais significativa é seu suporte assíncrono por meio da interfaceCompletionStage do Java 8 e suporte para fluxos reativos. Como veremos abaixo, o Lettuce oferece uma interface natural para fazer solicitações assíncronas do servidor de banco de dados Redis e para criar fluxos.

Ele também usa o Netty para se comunicar com o servidor. Isso cria uma API "mais pesada", mas também é mais adequada para compartilhar uma conexão com mais de um thread.

3. Configuração

3.1. Dependência

Vamos começar declarando a única dependência de que precisaremos empom.xml:


    io.lettuce
    lettuce-core
    5.0.1.RELEASE

A versão mais recente da biblioteca pode ser verificada emGithub repository ouMaven Central.

3.2. Instalação do Redis

Precisamos instalar e executar pelo menos uma instância do Redis, duas se quisermos testar o clustering ou o modo sentinela (embora o modo sentinela exija três servidores para funcionar corretamente). última versão estável neste momento.

Mais informações sobre como começar a usar o Redis podem ser encontradashere, incluindo downloads para Linux e MacOS.

O Redis oficialmente não oferece suporte ao Windows, mas há uma porta do servidorhere. Também podemos executar o Redis emDocker, que é uma alternativa melhor para o Windows 10 e uma maneira rápida de começar a funcionar.

4. Conexões

4.1. Conectando a um servidor

A conexão com o Redis consiste em quatro etapas:

  1. Criando um URI Redis

  2. Usando o URI para se conectar a umRedisClient

  3. Abrindo uma conexão Redis

  4. Gerando um conjunto deRedisCommands

Vamos ver a implementação:

RedisClient redisClient = RedisClient
  .create("redis://[email protected]:6379/");
StatefulRedisConnection connection
 = redisClient.connect();

AStatefulRedisConnection é o que parece; uma conexão thread-safe com um servidor Redis que manterá sua conexão com o servidor e se reconectará, se necessário. Depois de termos uma conexão, podemos usá-lo para executar comandos Redis de forma síncrona ou assíncrona.

RedisClient usa recursos substanciais do sistema, pois mantém recursos Netty para comunicação com o servidor Redis. Os aplicativos que requerem várias conexões devem usar um únicoRedisClient.

4.2. URIs do Redis

Criamos umRedisClient passando um URI para o método de fábrica estático.

A alface utiliza uma sintaxe personalizada para URIs Redis. Este é o esquema:

redis :// [[email protected]] host [: port] [/ database]
  [? [timeout=timeout[d|h|m|s|ms|us|ns]]
  [&_database=database_]]

Existem quatro esquemas de URI:

  • redis - um servidor Redis autônomo

  • rediss - um servidor Redis autônomo por meio de uma conexão SSL

  • redis-socket - um servidor Redis autônomo através de um soquete de domínio Unix

  • redis-sentinel - um servidor Redis Sentinel

A instância do banco de dados Redis pode ser especificada como parte do caminho da URL ou como um parâmetro adicional. Se ambos forem fornecidos, o parâmetro terá maior precedência.

No exemplo acima, estamos usando uma representaçãoString. Lettuce também tem uma classeRedisURI para construir conexões. Ele oferece o padrãoBuilder:

RedisURI.Builder
  .redis("localhost", 6379).auth("password")
  .database(1).build();

E um construtor:

new RedisURI("localhost", 6379, 60, TimeUnit.SECONDS);

4.3. Comandos Síncronos

Semelhante ao Jedis, o Lettuce fornece um conjunto completo de comandos do Redis na forma de métodos.

No entanto, o Lettuce implementa as versões síncrona e assíncrona. Analisaremos brevemente a versão síncrona e usaremos a implementação assíncrona para o restante do tutorial.

Depois de criar uma conexão, usamos para criar um conjunto de comandos:

RedisCommands syncCommands = connection.sync();

Agora, temos uma interface intuitiva para se comunicar com o Redis.

Podemos definir e obterString values:

syncCommands.set("key", "Hello, Redis!");

String value = syncommands.get(“key”);

Podemos trabalhar com hashes:

syncCommands.hset("recordName", "FirstName", "John");
syncCommands.hset("recordName", "LastName", "Smith");
Map record = syncCommands.hgetall("recordName");

Abordaremos mais Redis mais adiante neste artigo.

A API síncrona do Lettuce usa a API assíncrona. O bloqueio é feito para nós no nível do comando. This means that more than one client can share a synchronous connection.

4.4. Comandos Assíncronos

Vamos dar uma olhada nos comandos assíncronos:

RedisAsyncCommands asyncCommands = connection.async();

Recuperamos um conjunto deRedisAsyncCommands da conexão, semelhante a como recuperamos o conjunto síncrono. Esses comandos retornamRedisFuture (que é aCompletableFuture internamente):

RedisFuture result = asyncCommands.get("key");

Um guia para trabalhar comCompletableFuture pode ser encontradohere.

4.5. API reativa

Por fim, vamos ver como trabalhar com a API reativa sem bloqueio:

RedisStringReactiveCommands reactiveCommands = connection.reactive();

Esses comandos retornam resultados agrupados emMono ouFlux deProject Reactor.

Um guia para trabalhar com o Project Reactor pode ser encontradohere.

5. Estruturas de dados Redis

Vimos rapidamente as strings e hashes acima, vamos ver como o Lettuce implementa o resto das estruturas de dados do Redis. Como seria de se esperar, cada comando Redis tem um método com nome semelhante.

5.1. Listas

ValoresLists are lists of Strings with the order of insertion preserved. são inseridos ou recuperados de qualquer uma das extremidades:

asyncCommands.lpush("tasks", "firstTask");
asyncCommands.lpush("tasks", "secondTask");
RedisFuture redisFuture = asyncCommands.rpop("tasks");

String nextTask = redisFuture.get();

Neste exemplo,nextTask é igual a “firstTask“. Lpush envia valores para o topo da lista e, em seguida,rpop exibe valores do final da lista.

Também podemos destacar elementos do outro lado:

asyncCommands.del("tasks");
asyncCommands.lpush("tasks", "firstTask");
asyncCommands.lpush("tasks", "secondTask");
redisFuture = asyncCommands.lpop("tasks");

String nextTask = redisFuture.get();

Começamos o segundo exemplo removendo a lista comdel. Em seguida, inserimos os mesmos valores novamente, mas usamoslpop para retirar os valores do topo da lista, de forma quenextTask contenha o texto “secondTask”.

5.2. Sets

Conjuntos Redis são coleções não ordenadas deStrings semelhantes a JavaSets; não há elementos duplicados:

asyncCommands.sadd("pets", "dog");
asyncCommands.sadd("pets", "cat");
asyncCommands.sadd("pets", "cat");

RedisFuture> pets = asyncCommands.smembers("nicknames");
RedisFuture exists = asyncCommands.sismember("pets", "dog");

Quando recuperamos o Redis definido comoSet, o tamanho é dois, já que o“cat” duplicado foi ignorado. Quando consultamos o Redis para a existência de“dog” comsismember,, a resposta étrue.

5.3. Hashes

Examinamos brevemente um exemplo de hashes anteriormente. Eles valem uma explicação rápida.

Redis Hashes are records with String fields and values. Cada registro também tem uma chave no índice primário:

asyncCommands.hset("recordName", "FirstName", "John");
asyncCommands.hset("recordName", "LastName", "Smith");

RedisFuture lastName
  = syncCommands.hget("recordName", "LastName");
RedisFuture> record
  = syncCommands.hgetall("recordName");

Usamoshset para adicionar campos ao hash, passando o nome do hash, o nome do campo e um valor.

Em seguida, recuperamos um valor individual comhget, o nome do registro e o campo. Por fim, buscamos todo o registro como um hash comhgetall.

5.4. Conjuntos classificados

Sorted Sets contains values and a rank, by which they are sorted. A classificação é um valor de ponto flutuante de 64 bits.

Os itens são adicionados com uma classificação e recuperados em um intervalo:

asyncCommands.zadd("sortedset", 1, "one");
asyncCommands.zadd("sortedset", 4, "zero");
asyncCommands.zadd("sortedset", 2, "two");

RedisFuture> valuesForward = asyncCommands.zrange(key, 0, 3);
RedisFuture> valuesReverse = asyncCommands.zrevrange(key, 0, 3);

O segundo argumento parazadd é uma classificação. Recuperamos um intervalo por classificação comzrange para ordem crescente ezrevrange para decrescente.

Nós adicionamos “zero” com uma classificação de 4, então ele aparecerá no final devaluesForwarde no início devaluesReverse.

6. Transações

As transações permitem a execução de um conjunto de comandos em uma única etapa atômica. Esses comandos são garantidos para serem executados em ordem e exclusivamente. Os comandos de outro usuário não serão executados até que a transação seja concluída.

Todos os comandos são executados ou nenhum deles. Redis will not perform a rollback if one of them fails. Depois queexec() é chamado, todos os comandos são executados na ordem especificada.

Vejamos um exemplo:

asyncCommands.multi();

RedisFuture result1 = asyncCommands.set("key1", "value1");
RedisFuture result2 = asyncCommands.set("key2", "value2");
RedisFuture result3 = asyncCommands.set("key3", "value3");

RedisFuture execResult = asyncCommands.exec();

TransactionResult transactionResult = execResult.get();

String firstResult = transactionResult.get(0);
String secondResult = transactionResult.get(0);
String thirdResult = transactionResult.get(0);

A chamada paramulti inicia a transação. Quando uma transação é iniciada, os comandos subsequentes não são executados até queexec() seja chamado.

No modo síncrono, os comandos retornamnull.. No modo assíncrono, os comandos retornamRedisFuture. Exec retorna umTransactionResult que contém uma lista de respostas.

Como osRedisFutures também recebem seus resultados, os clientes da API assíncrona recebem o resultado da transação em dois lugares.

7. Lote

Sob condições normais, o Lettuce executa comandos assim que são chamados por um cliente de API.

É isso que a maioria dos aplicativos normais deseja, especialmente se eles dependem do recebimento de resultados de comandos em série.

No entanto, esse comportamento não é eficiente se os aplicativos não precisam de resultados imediatamente ou se grandes quantidades de dados estão sendo carregados em massa.

Aplicativos assíncronos podem substituir esse comportamento:

commands.setAutoFlushCommands(false);

List> futures = new ArrayList<>();
for (int i = 0; i < iterations; i++) {
    futures.add(commands.set("key-" + i, "value-" + i);
}
commands.flushCommands();

boolean result = LettuceFutures.awaitAll(5, TimeUnit.SECONDS,
  futures.toArray(new RedisFuture[0]));

Com setAutoFlushCommands definido comofalse, o aplicativo deve chamarflushCommands manualmente. Neste exemplo, enfileiramos vários comandosset e liberamos o canal. AwaitAll espera que todos osRedisFutures sejam concluídos.

Este estado é definido por conexão e afeta todos os threads que usam a conexão. Este recurso não se aplica a comandos síncronos.

8. Publish/Subscribe

O Redis oferece um sistema simples de mensagens de publicação / assinatura. Os assinantes consomem mensagens de canais com o comandosubscribe. As mensagens não são persistentes; eles são entregues aos usuários apenas quando eles estão inscritos em um canal.

O Redis usa o sistema pub / subs para notificações sobre o conjunto de dados Redis, oferecendo aos clientes a capacidade de receber eventos sobre chaves definidas, excluídas, expiradas, etc.

Veja a documentaçãohere para mais detalhes.

8.1. Assinante

ARedisPubSubListener recebe mensagens pub / sub. Esta interface define vários métodos, mas mostraremos apenas o método para receber mensagens aqui:

public class Listener implements RedisPubSubListener {

    @Override
    public void message(String channel, String message) {
        log.debug("Got {} on channel {}",  message, channel);
        message = new String(s2);
    }
}

UsamosRedisClient para conectar um canal pub / sub e instalar o ouvinte:

StatefulRedisPubSubConnection connection
 = client.connectPubSub();
connection.addListener(new Listener())

RedisPubSubAsyncCommands async
 = connection.async();
async.subscribe("channel");

Com um listener instalado, recuperamos um conjunto deRedisPubSubAsyncCommandse nos inscrevemos em um canal.

8.2. Editor

A publicação é apenas uma questão de conectar um canal Pub / Sub e recuperar os comandos:

StatefulRedisPubSubConnection connection
  = client.connectPubSub();

RedisPubSubAsyncCommands async
  = connection.async();
async.publish("channel", "Hello, Redis!");

A publicação requer um canal e uma mensagem.

8.3. Assinaturas reativas

O Lettuce também oferece uma interface reativa para assinar mensagens de pub / sub:

StatefulRedisPubSubConnection connection = client
  .connectPubSub();

RedisPubSubAsyncCommands reactive = connection
  .reactive();

reactive.observeChannels().subscribe(message -> {
    log.debug("Got {} on channel {}",  message, channel);
    message = new String(s2);
});
reactive.subscribe("channel").subscribe();

OFlux retornado porobserveChannels recebe mensagens para todos os canais, mas como se trata de um fluxo, a filtragem é fácil de fazer.

9. Alta disponibilidade

O Redis oferece várias opções para alta disponibilidade e escalabilidade. O entendimento completo requer conhecimento das configurações do servidor Redis, mas iremos apresentar uma breve visão geral de como o Lettuce as suporta.

9.1. Master/Slave

Os servidores Redis se replicam em uma configuração mestre / escravo. O servidor principal envia ao escravo um fluxo de comandos que replicam o cache principal para o escravo. Redis doesn’t support bi-directional replication, so slaves are read-only.

O Lettuce pode se conectar aos sistemas Master / Slave, consultar a topologia e selecionar escravos para operações de leitura, o que pode melhorar o rendimento:

RedisClient redisClient = RedisClient.create();

StatefulRedisMasterSlaveConnection connection
 = MasterSlave.connect(redisClient,
   new Utf8StringCodec(), RedisURI.create("redis://localhost"));

connection.setReadFrom(ReadFrom.SLAVE);

9.2. Sentinela

O Redis Sentinel monitora instâncias mestras e escravas e orquestra failovers a escravos no caso de um failover mestre.

O Lettuce pode se conectar ao Sentinel, usá-lo para descobrir o endereço do mestre atual e, em seguida, retornar uma conexão para ele.

Para fazer isso, construímos umRedisURI diferente e conectamos nossoRedisClient a ele:

RedisURI redisUri = RedisURI.Builder
  .sentinel("sentinelhost1", "clustername")
  .withSentinel("sentinelhost2").build();
RedisClient client = new RedisClient(redisUri);

RedisConnection connection = client.connect();

Criamos o URI com o nome do host (ou endereço) do primeiro Sentinel e um nome de cluster, seguido por um segundo endereço do sentinela. Quando nos conectamos ao Sentinel, o Lettuce a consulta sobre a topologia e retorna uma conexão ao servidor principal atual para nós.

A documentação completa está disponívelhere.

9.3. Clusters

O Redis Cluster usa uma configuração distribuída para fornecer alta disponibilidade e alto rendimento.

Clusters de chaves de fragmentos em até 1000 nós, portanto, as transações não estão disponíveis em um cluster:

RedisURI redisUri = RedisURI.Builder.redis("localhost")
  .withPassword("authentication").build();
RedisClusterClient clusterClient = RedisClusterClient
  .create(rediUri);
StatefulRedisClusterConnection connection
 = clusterClient.connect();
RedisAdvancedClusterCommands syncCommands = connection
  .sync();

RedisAdvancedClusterCommands contém o conjunto de comandos Redis suportados pelo cluster, roteando-os para a instância que contém a chave.

Uma especificação completa está disponívelhere.

10. Conclusão

Neste tutorial, vimos como usar o Lettuce para conectar e consultar um servidor Redis de dentro do nosso aplicativo.

O Lettuce suporta o conjunto completo de recursos Redis, com o bônus de uma interface assíncrona completamente segura para threads. Ele também faz uso extensivo da interfaceCompletionStage do Java 8 para dar aos aplicativos um controle refinado sobre como eles recebem os dados.

Amostras de código, como sempre, podem ser encontradasover on GitHub.