Redis básico: tipos de dados e quando usar

1. Visão geral

Redis é um in-memory data store, frequentemente entendido erroneamente como “apenas para cache”. Na verdade, Redis fornece vários tipos de dados (data structures) diferentes, cada um adequado para um grupo de problemas específico.

Este artigo ajuda você a dominar os 5 tipos de dados mais comuns do Redis, como usá-los via Spring Data Redis e, mais importante: quando escolher cada um. Após ler, você saberá como evitar o erro clássico de “usar String para tudo” e depois sofrer ao atualizar os dados.

2. Preparando o ambiente

Adicione a dependency Spring Data Redis ao projeto:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Declare a conexão no application.yml:

spring:
  data:
    redis:
      host: localhost
      port: 6379

Execute o Redis com Docker rapidamente:

docker run -d --name redis-dev -p 6379:6379 redis:7-alpine

Os exemplos abaixo usam RedisTemplate<String, String> que o Spring Boot configura automaticamente, você não precisa declarar o bean manualmente.

3. String: o tipo mais simples

String é o tipo básico, armazena um value associado a uma key. O value pode ser texto, número ou JSON serializado.

@Service
public class SessionTokenService {

    private final RedisTemplate<String, String> redisTemplate;

    public SessionTokenService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void saveToken(String userId, String token) {
        redisTemplate.opsForValue()
            .set("session:" + userId, token, Duration.ofMinutes(30));
    }

    public String getToken(String userId) {
        return redisTemplate.opsForValue().get("session:" + userId);
    }
}
  • set(key, value, ttl): salva o token com tempo de vida de 30 minutos, Redis o deleta automaticamente quando expira.
  • get(key): retorna null se a key não existe ou já expirou.

Saída ao chamar getToken com token expirado:

null

Quando usar: token de sessão, cache de um objeto já serializado (JSON), contador simples (INCR), feature flag. String também suporta incremento atômico:

Long visits = redisTemplate.opsForValue().increment("page:views:home");

Observação: não salve um objeto grande e serialize/desserialize tudo apenas para atualizar 1 campo — é um sinal de que você deve usar Hash.

4. Hash: objeto com múltiplos campos

Hash é como um Map<String, String> dentro de uma key, permitindo atualizar cada campo individualmente sem precisar ler/escrever o objeto inteiro.

@Service
public class UserProfileCache {

    private final RedisTemplate<String, String> redisTemplate;

    public UserProfileCache(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void saveProfile(String userId, Map<String, String> fields) {
        redisTemplate.opsForHash().putAll("user:" + userId, fields);
    }

    public void updateEmail(String userId, String newEmail) {
        redisTemplate.opsForHash().put("user:" + userId, "email", newEmail);
    }

    public Map<Object, Object> getProfile(String userId) {
        return redisTemplate.opsForHash().entries("user:" + userId);
    }
}
  • putAll: escreve múltiplos campos ao mesmo tempo, semelhante a HSET.
  • put um campo: atualiza apenas o campo email, outros campos (name, age…) não são afetados.
  • entries: obtém todos os pares campo-valor do hash.

Comparado com String: se usar String + JSON, toda vez que mudar email você precisa GET o JSON inteiro, fazer parse, editar, serializar novamente e SET. Com Hash, apenas um comando HSET.

Quando usar: objetos com múltiplos campos ou atualizações independentes — perfil de usuário, detalhes de produto, configuração por tenant.

5. List: fila e histórico em ordem

List é uma lista ordenada, suporta push/pop em ambas as extremidades. Ideal para filas ou armazenar os N itens mais recentes.

@Service
public class RecentActivityService {

    private final RedisTemplate<String, String> redisTemplate;

    public RecentActivityService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void logActivity(String userId, String activity) {
        String key = "activity:" + userId;
        redisTemplate.opsForList().leftPush(key, activity);
        redisTemplate.opsForList().trim(key, 0, 9); // mantém os 10 itens mais recentes
    }

    public List<String> getRecentActivities(String userId) {
        return redisTemplate.opsForList().range("activity:" + userId, 0, -1);
    }
}
  • leftPush: adiciona a nova atividade no início da lista.
  • trim(key, 0, 9): reduz a lista para manter apenas os 10 primeiros elementos, evitando que a lista cresça indefinidamente.
  • range(0, -1): obtém todos os elementos, o índice -1 significa o último elemento.

Quando usar: timeline de atividades recentes, fila de jobs simples (combine com BLPOP para o consumidor aguardar), pilha de desfazer.

Observação: List não é apropriada se você precisa buscar por value ou acessar aleatoriamente com frequência — a complexidade de acesso por índice é O(n) para listas grandes.

6. Set: conjunto único, sem ordem

Set armazena valores únicos, suporta operações de conjunto (união, interseção, diferença) muito rapidamente.

@Service
public class TagService {

    private final RedisTemplate<String, String> redisTemplate;

    public TagService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void addTags(String articleId, String... tags) {
        redisTemplate.opsForSet().add("article:tags:" + articleId, tags);
    }

    public boolean hasTag(String articleId, String tag) {
        return redisTemplate.opsForSet()
            .isMember("article:tags:" + articleId, tag);
    }

    public Set<String> commonTags(String articleId1, String articleId2) {
        return redisTemplate.opsForSet()
            .intersect("article:tags:" + articleId1, "article:tags:" + articleId2);
    }
}
  • add: adiciona múltiplas tags, Redis remove automaticamente duplicatas.
  • isMember: verifica existência com complexidade O(1), muito mais rápido que varrer uma List.
  • intersect: encontra tags comuns entre dois artigos — usado para sugerir artigos relacionados.

Quando usar: tags/categorias, lista de visitantes únicos por dia (SADD visitors:2026-10-05 userId), verificação rápida de associação (por exemplo: o usuário já se registrou no evento?).

7. Sorted Set: classificação por pontuação

Sorted Set (ZSet) é como um Set, mas cada membro tem um score para ordenação. Este é o tipo mais poderoso para problemas de leaderboard.

@Service
public class LeaderboardService {

    private final RedisTemplate<String, String> redisTemplate;

    public LeaderboardService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void addScore(String playerId, double score) {
        redisTemplate.opsForZSet().add("leaderboard:weekly", playerId, score);
    }

    public Long getRank(String playerId) {
        // rank baseado em 0, null se não existe
        return redisTemplate.opsForZSet().reverseRank("leaderboard:weekly", playerId);
    }

    public Set<String> getTop10() {
        return redisTemplate.opsForZSet()
            .reverseRange("leaderboard:weekly", 0, 9);
    }
}
  • add(key, member, score): adiciona ou atualiza a pontuação do jogador.
  • reverseRank: obtém a classificação em ordem decrescente de pontuação (classificação mais alta tem maior pontuação).
  • reverseRange(0, 9): obtém os top 10, Redis já os ordena, sem necessidade de sort na aplicação.

Saída de exemplo ao chamar getTop10():

["playerA", "playerC", "playerB"]

Quando usar: tabela de classificação, fila de prioridade (score = prioridade), rate limiting por janela de tempo (score = timestamp).

8. Testes com JUnit 5 e Testcontainers

Para testar a lógica relacionada ao Redis sem afetar dados reais, use Testcontainers.

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
class LeaderboardServiceTest {

    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
        .withExposedPorts(6379);

    @DynamicPropertySource
    static void redisProps(DynamicPropertyRegistry registry) {
        registry.add("spring.data.redis.host", redis::getHost);
        registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
    }

    @Autowired
    private LeaderboardService leaderboardService;

    @Test
    void shouldReturnTopPlayersInOrder() {
        leaderboardService.addScore("playerA", 100);
        leaderboardService.addScore("playerB", 50);
        leaderboardService.addScore("playerC", 80);

        Set<String> top = leaderboardService.getTop10();

        assertThat(top).containsExactly("playerA", "playerC", "playerB");
    }
}
  • @Container inicializa um Redis real em Docker para cada classe de teste.
  • @DynamicPropertySource substitui host/port para que o Spring Boot se conecte ao container correto.
  • containsExactly verifica a ordem exata — Sorted Set garante que o resultado sempre seja ordenado por score decrescente.

9. Tabela de comparação rápida

Tipo Estrutura Usar quando
String key → value cache de objeto, contador, token de sessão
Hash key → mapa campo-valor objetos com múltiplos campos, atualizações parciais
List key → lista ordenada fila, itens recentes, timeline
Set key → conjunto único tags, verificação de associação, operações de conjunto
Sorted Set key → membro com score leaderboard, rate limiting, fila de prioridade

10. Erros comuns

  • Usar String para dados que atualizam frequentemente: causa race condition quando múltiplas threads leem-modificam-escrevem JSON simultaneamente. Use Hash para atualizações atômicas por campo.
  • List sem trim: a lista cresce indefinidamente, consome RAM, comando LRANGE fica cada vez mais lento com o tempo.
  • Esquecer de definir TTL: Redis não deleta automaticamente as keys por padrão, dados obsoletos se acumulam indefinidamente. Sempre considere expire para dados temporários como sessão, cache.
  • Usar Set para dados que precisam de ordem: Set não garante ordem de iteração, use Sorted Set se precisar ordenar.

Observação: Redis é in-memory — todos os dados residem na RAM. Não armazene dados grandes, de longa duração ou dados que requerem transações ACID complexas (por exemplo: saldo bancário) no Redis como única fonte de verdade (source of truth).

11. Conclusão

Redis não é apenas um “cache grande”, mas um conjunto de ferramentas de data structure flexível. Escolher o tipo de dado certo — String para valores simples, Hash para objetos com múltiplos campos, List para filas, Set para associações, Sorted Set para classificações — resulta em código mais conciso, menos comandos e aproveita o poder das operações atômicas do Redis em vez de lidar com essa lógica na camada de aplicação.

rediscachingspring-bootnosqlperformance