Redis I: Cache distribuído

iundarigun
Dev Cave
Published in
7 min readJan 28, 2020

Faz tempo que estou ensaiando para escrever sobre Redis. Temos uma brincadeira entre nós: toda vez que alguém precisa se ausentar ou avisa que vai chegar tarde, falamos para ele:

Boa entrevista! E lembre-se, Kafka é streaming, Rabbit é fila, Redis é cache e Cassandra é banco.

Sem ser mentira, diria que é tremendamente injusto. Na Wavy tive a oportunidade de ver ele funcionando e até implementei uma solução que usava Redis, e não era usado como cache. Nesta serie de posts (acredito que serão três), vou detalhar alguns casos de uso de Redis.

Ao final, o que é Redis?

Redis significa REmote DIctionary Server e, segundo a definição, é um armazenamento de estrutura de dados na memória, usado como banco de dados, cache e broker de mensagens. Entra dentro do bolo de bancos de dados NoSQL, na categoria de chave-valor. Ele é considerado extremamente rápido na escrita e na leitura, por manter os dados em memória. O cache é um dos usos, provavelmente o mais estendido.

Redis em local

Para rodar uma instância do Redis em local, podemos realizar a instalação ou podemos usar docker. É tremendamente simples:

docker run --name local-redis -p 6379:6379 -d redis

Só para ter uma noção do funcionamento, iremos executar alguns comandos simples via CLI:

docker exec -it local-redis redis-cli

Para criar uma chave com um valor:

127.0.0.1:6379> SET “my first key” “Welcome to redis”

Para recuperar o valor:

127.0.0.1:6379> GET “my first key”

E para ver todas as chaves do Redis:

127.0.0.1:6379> KEYS *

O valor que colocamos depois do KEYS é uma expressão regular, então podemos usá-lo para procurar chaves de determinado formato.
Nota: Um formato de chave bem estendidos é “some:words:here”, separando por dois pontos várias palavras numa estrutura que faça sentido no uso.

Um das funcionalidades mais usadas é a politica de expiração de chaves, ponto que facilita o uso como cache:

127.0.0.1:6379> SET "some:words:here" "Wellcome with expiration" EX 20

No exemplo acima, criamos uma chave com uma duração de 20 segundos. Para consultar o tempo de expiração restante, podemos executar o seguinte comando:

127.0.0.1:6379> TTL "some:words:here"

O Redis preza pela simplicidade. Como vimos, só criar o container de docker (ou levantar a aplicação instalada em local) e abrimos um CLI. Não precisou autenticação nem criação de usuário nem de schema ou banco de dados.

O Redis suporta, além de Strings como os do exemplo, listas, hashMap, set, sorted set, bitmap entre outros. Não vou me aprofundar mais no comandos via CLI, mas podem dar uma olhada no README do repositório do meu Github ou consultar este artigo da Movile.

Cache distribuído com Redis

Eu sei que falei que ia desmitificar que o Redis só serve para cache, mas primeiro achei interessante explicar como usar o Redis justamente para cache distribuído, usando Spring Boot e Kotlin. Para ver o código, como sempre pode fazer o checkout do repositório:

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

O projeto está na pasta distributed-cache. Criamos um projeto que expõe uma API Rest “fingindo” um acesso a banco de dados, com umas poucos dependências:

val swaggerVersion = "2.9.2"
val fakerVersion = "1.0.1"

dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

implementation("io.springfox:springfox-swagger2:$swaggerVersion")
implementation("io.springfox:springfox-swagger-ui:$swaggerVersion")
implementation("com.github.javafaker:javafaker:$fakerVersion")
}

Para ativar o cache, precisamos de duas coisas. A primeira é ativar o uso do cache, simplesmente com a annotation EnableCaching.

A segunda é configurar o Redis no application.yml. Vamos indicar o tipo de cache que usaremos também:

spring:
redis:
host: localhost
port: 6379
cache:
type: redis

Para usar o cache, usamos a annotation Cacheable e indicamos o nome que queremos usar para identificar o cache.

Vamos levantar a aplicação e fazer trabalhar o cache. Mas antes disso, o Redis oferece uma ferramenta de monitoração e, ativando ela, podemos ver os comandos executados pela aplicação. Vamos entrar no CLI como mostramos antes e executar o seguinte comando:

127.0.0.1:6379> MONITOR

Se executamos o findById usando o swagger http://localhost:1980/swagger-ui.html podemos ver nos logs do Redis que primeiro buscamos a chave “findById::123” e como não existe, setamos a chave com o valor do resultado do findById.

Se reparamos melhor, veremos que não foi setado nenhum tempo de expiração, e isso é porque não especificamos valor algum relacionado a isso.

Vamos alterar o application.yml:

spring:
redis:
host: localhost
port: 6379
cache:
type: redis
redis:
time-to-live: 20_000

Agora indicamos que o tempo de expiração da chave é da 20.000 milissegundos, 20 segundos. Vamos executar o findById e o count:

“PX” “20000” significa tempo de expiração de 20.000 ms

O que vemos é que a chave do findById é lida mas como existe da última execução, não é setada com o tempo de expiração correto. No caso do count, porém, sim que é setado. Isso é um ponto de alerta: Lembre de limpar as chaves criadas de forma errada ou antigas. Para fazer isso por CLI:

127.0.0.1:6379> del "findById::123"

Configuração (um pouco) avançada

Vimos que podemos especificar um tempo de expiração (TTL de aqui para frente) genérico para todas as chaves. Nem sempre as chaves terão o mesmo TTL. Também vimos que as mensagens são guardadas no Redis num formato não legível por nós. Como fazemos então para customizar essas opções?

Vamos então configurar o cache na mão. Podemos deixar o application.yml do jeito que estava antes de configurar o TTL. Vamos criar na mão três comportamentos na classe CacheConfiguration:
- TTL diferentes em função do nome do cache
- Salvar um dos cache em formato json
- Alterar o padrão da chave

Como vemos no código, precisamos um RedisCacheWriter (redis oferece um blocante e outro não blocante) e um RedisCacheConfiguration, onde definimos tanto o TTL, como o formato do prefixo da chave e o serializer usado para formatar o conteúdo da chave. E assim geramos o CacheManager, que será o responsável pelo gerenciamento.

Se executamos o findById e o count podemos ver no Redis que o comportamento é diferente nos dois casos, conforme configurado:

O que acontece com aqueles caches que não configuramos especificamente? Vão ter o mesmo comportamento que no primeiro teste que fizemos. Para evitar isso, temos duas opções, uma mais laxa, especificando uma configuração default e outra mais restritiva, não permitindo a criação de caches “inesperados”:

Renovar o TTL

Uma das opções que talvez esteja sentindo falta é a renovação de cache como tem no Caffeine (e precisamente nos exemplos que coloquei no código não faz muito sentido). Até onde eu sei, não existe essa opção de forma automatizada. Mas, claro, podemos implementar de forma simples nosso próprio RedisCacheWriter e forçar esse comportamento.

No projeto criei a classe CustomRedisCacheWriter que implementa justamente o RedisCacheWriter. Para simplificar, passei por parâmetros no construtor a connection do Redis e uma duração fixa. Cada vez que procura um objeto que já está no Redis, o TTL é settado de novo.

Agora só precisa alterar o CacheConfiguration para criar e usar este writer. Se rodamos a aplicação e acessamos ao método count teremos os seguintes logs no Redis:

O problema da classe é que temos um TTL para todos os cache, mas precisaríamos ter vários, como acontece no put. Não coloquei para simplificar a lógica, mas seria tão simples como ter um map do nome do cache e o TTL desse cache.

Distribuído

Por último, como é um banco de dados, quando levantamos duas ou mais instâncias da aplicação, todas ficam conectadas no mesmo Redis. Qualquer consulta de Redis é feita no mesmo banco.

$ java -jar -Dserver.port=1980 distributed-cache-0.0.1-SNAPSHOT.jar
$ java -jar -Dserver.port=1989 distributed-cache-0.0.1-SNAPSHOT.jar

Vamos fazer as seguintes consultas com um segundo de tempo entre elas e monitorar o redis:

$ curl http://localhost:1980/employees/count
$ curl http://localhost:1989/employees/count
$ curl http://localhost:1989/employees/count
$ curl http://localhost:1980/employees/count
$ curl http://localhost:1980/employees/count
$ curl http://localhost:1989/employees/count

Se monitoramos o redis, temos a seguinte saída:

Só a primeira consulta gerou um set no Redis. As outas reaproveitaram os dados, independentemente da origem da consulta.

Conclusão

Como vimos, fazer cache distribuído com Redis é muito simples, basicamente nos aproveitamos da performance dele para transformar um acesso a banco num cache.

O primeiro post sobre Redis chega até aqui. Reparem que não falei nada sobre como fazer eviction, put e outras opções que podemos usar no cache porque queria focar no uso do Redis.

No próximo post pretendo trazer alguns casos de uso para o Redis além do cache.

Referências

--

--

iundarigun
Dev Cave

Java and Kotlin software engineer at Clearpay