Redis II: Além do cache

iundarigun
Dev Cave
Published in
6 min readFeb 11, 2020

No post anterior sobre Redis, focamos no uso como cache distribuído. Como explicamos, o Redis é muito mais que cache. Hoje vou falar do seu uso como banco de dados.

Como funciona por baixo do panos

Por definição o Redis é um in-memory storage. Então, como podemos usá-lo como banco de dados? O Redis permite dois tipos de configuração referente ao storage:

  • RDB: É a forma mais simples. Os dados são persistidos de forma assíncrona de tempos em tempos. Podemos configurar a cadência em que o Redis salva os dados, mas se o servidor parar, todas as alterações após o último save serão perdidas.
  • AOF: É guardado um log de cada operação. Desta forma, quando reiniciar, o Redis reconstrói os dados a partir desse log. Os dados não serão perdidos mas a performance pode ser comprometida.

Cada um dos tipos tem várias configurações para fazer um fine tunning, mas não pretendo entrar em mais detalhes por ora. No README do meu repositório do git tem algumas informações complementares ou pode ler o livro da casa do código sobre Redis.

Como usar no Spring Boot

Criei um projeto para mostrar como conectar no Redis com Spring Boot e Kotlin. Pode fazer checkout do repositório:

> git clone https://github.com/iundarigun/learning-redis

O projeto está na pasta more-than-cache. A aplicação é bem simples, só espera receber o cadastro de instâncias no ar de uma aplicação. Elas são registradas dentro de uma estrutura tipo set e também é registrada uma chave individual para cada instância com um TTL definido.

A ideia é que as instâncias mandem um heartbeat a cada x tempo para evitar ser removida da lista. Quando precisamos saber que instâncias estão funcionando, verificamos se alguma expirou. O exemplo é bem bobo, mas para o que queríamos mostrar acredito que atende.

O que precisamos para consultar o “banco” é um bean do tipo RedisTemplate. Vamos mostrar uma bean criado pelo próprio Spring primeiro. A classe se chama StringRedisTemplate e basicamente é usado para trabalhar com chaves e conteúdo do tipo texto, por isso a classe estende da RedisTemplate, que espera determinar um tipo para a chave e os valores e, no caso, é String:

Quando usamos ela (ou qualquer bean RedisTemplate), temos disponível vários métodos de acesso ao Redis e focadas nas diferentes estruturas de dados disponíveis:

Criamos uma classe repository que será encarregada de lidar com o acesso ao banco:

Usamos o tipo de dados SET para guardar as instâncias que estão registradas para a aplicação.

Também criamos uma classe tipo repository para gerenciar as informações das instâncias:

Basicamente, usamos um tipo de dados simples, e especificamos um TTL. O detalhe que queria mostrar é que usamos um RedisTemplate diferente, pensando em salvar um objeto. Mas este bean não é o Spring quem fornece, somos nós que precisamos criá-lo. Isso é feito no RedisConfiguration:

Na classe ApplicationService, é verificado se o objeto expirou:

Bom, é um exemplo simples mas ilustra bem como lidar com Redis como banco de dados, combinando tipo de dados diferentes e dados “permanentes” e dados expiráveis.

Gerenciamento de jobs

Uma das coisas que sempre achei difícil de escalar são os jobs. E não só isso, como gerenciar uma aplicação que tem várias instâncias mas os jobs não estão preparados para ser executados em paralelo? Podemos resolver essa situação com o Redis.

Locando a execução

Uma das caraterísticas principais do Redis é que todos os comandos executados são atômicos, e isso é garantido em como ele funciona. Podemos então usar uma estratégia para settar uma chave-valor que só tem sucesso se a chave não existir. Na nossa aplicação, criamos um job da seguinte forma:

O job é executado cada hora e o primeiro que tentamos é settar um valor aleatório para a chave no caso de não existir, com um TTL de 5 minutos. Caso conseguir, a instrução devolve true e podemos continuar com a execução.

Para testar, executamos duas aplicações em paralelo e o resultado foi o seguinte:

Uma das instâncias adquiriu o lock

Ativamos também o monitor para ver que instruções foram executadas no Redis, e observamos que foram executadas as instruções SET com o parâmetro NX, que significa que só salva o valor se a chave não existe:

Distribuindo a execução

Imaginem um job que pega x registros do banco em função de certa configuração para tratar alguma coisa (sei, tudo muito vago… ). Mas a consulta dessa lista de registros se faz complicada de distribuir quando cada registro só pode ser lido uma vez.

Poderíamos usar a estratégia de lock do exemplo anterior e só uma instância executaria o job. Mas isso realmente não escala a execução. Uma possível solução seria dividir em dois jobs. Um primeiro que só é executado desde uma instância usando a estratégia de lock, pega os ids e divide entre as instâncias disponíveis (podemos usar o que vimos no inicio do post para saber quais são) colocando essa lista no Redis. O outro leria esses ids e faria o processamento daqueles ids assinados, paralelizando de fato a execução.

Como as instâncias podem ser dinâmicas, a lista deveria ter um TTL por se a instância for reiniciada antes de ler as informações.

Nota: Não implementei a solução acima para não estender muito o post.

Scripts em Lua

Como falamos um pouco antes, os comandos de Redis são executados de forma atômica. Isso significa que não haverá consulta/alteração do banco durante a execução do comando. As vezes, isso pode não ser suficiente, pois pode precisar executar um grupo de comandos de forma atômica.

Para isso, temos a execução de scripts com a linguagem Lua. Basicamente e resumindo muito, usamos a instrução EVAL do Redis para executar vários comandos. Veja um exemplo no README do meu repositório.

Para mostrar um exemplo, usamos a ideia de validação de instâncias vivas para fazer todo o trabalho com uma instrução só. Criamos um arquivo tipo lua na pasta resources/scripts:

Para executar o script, usamos o mesmo RedisTemplate:

O que temos aqui exatamente:

  • Precisamos criar um RedisScript que faz referência ao script e ao tipo de retorno.
  • Usamos o método execute do RedisTemplate. Recebe por parâmetros o RedisScript, uma lista de chaves e tantos argumentos extras como precisar (no exemplo não passamos nenhum).
  • No script, usamos as chaves como KEYS[#num] onde a primeira posição da lista é 1, e não 0. Os argumentos extras são referenciados como ARGV[#num] (também começando por 1).

Só precisamos tomar cuidado com a execução deste tipo de scripts porque podem gargalhar a aplicação, pois a execução é blocante. O timeout padrão para a execução é de 5 segundos, mas isso é configurável.

Testes integrados

Usando o Redis como banco, a necessidade de integra-lo dentro dos nossos testes automatizados pode ser maior que quando é usado como cache. Existem várias alternativas para embutir um servidor Redis nos testes, mas nos exemplo do projeto usei o embedded-redis do kstyrc, e testei o endpoint de ApplicationController usando Rest Assured. No build do gradle adicionamos as dependências seguintes:

// Other things
val embeddedRedisVersion = "0.6"
val restAssuredVersion = "4.1.2"

dependencies {
// Some other dependencies
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
testImplementation("com.github.kstyrc:embedded-redis:$embeddedRedisVersion")
testImplementation("io.rest-assured:rest-assured:$restAssuredVersion")
testImplementation("io.rest-assured:json-path:$restAssuredVersion")
testImplementation("io.rest-assured:xml-path:$restAssuredVersion")
}

Configuramos também o application.yml do resources do pacote de testes e especificamos uma porta diferente para o embedded Redis, para não conflitar com o Redis local:

spring:
application:
name: more-than-cache
profiles:
active: test
redis:
host: localhost
port: 6370
instances:
ttl: 1

Criamos uma configuração só para testes que levanta o Redis quando for chamada e para ele quando terminar de rodar:

Agora é só implementar os testes:

Só precisa tomar cuidado quando rodar em modo debug. Se você parar de forma abrupta a execução (mandando um kill do processo por exemplo), o embedded Redis pode ficar rodando e não conseguir executar mais os testes até matar o processo.

O que fica para o último post

Para o último post sobre Redis pretendo falar sobre algumas configurações (um pouco mais) avançadas e como podemos usar Redis como Pub Sub.

Referências

--

--

iundarigun
Dev Cave

Java and Kotlin software engineer at Clearpay