Redis básico: tipos de datos y cuándo usarlos

1. Descripción general

Redis es un almacén de datos en memoria, frecuentemente malinterpretado como algo que “solo sirve para cachear”. En realidad, Redis proporciona varios tipos de datos (data structures) diferentes, cada uno adecuado para un grupo específico de problemas.

Este artículo te ayudará a dominar los 5 tipos de datos más comunes de Redis, cómo usarlos con Spring Data Redis, y lo más importante: cuándo elegir cada tipo. Después de leerlo, sabrás evitar el error clásico de “usar String para todo” y luego sufrir cuando tengas que actualizar datos.

2. Preparar el entorno

Añade la dependencia de Spring Data Redis a tu proyecto:

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

Declara la conexión en application.yml:

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

Ejecuta Redis con Docker rápidamente:

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

Los ejemplos a continuación usan RedisTemplate<String, String> que Spring Boot configura automáticamente, no necesitas declarar el bean manualmente.

3. String: el tipo más simple

String es el tipo básico, almacena un value asociado a una key. El value puede ser texto, números, o 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): guarda el token con un tiempo de vida de 30 minutos, Redis lo elimina automáticamente cuando expira.
  • get(key): devuelve null si la key no existe o ha expirado.

Output cuando llamamos a getToken con un token expirado:

null

Cuándo usarlo: token de sesión, cachear un objeto ya serializado (JSON), contador simple (INCR), feature flag. String también soporta incremento atómico:

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

Nota: no guardes un objeto grande, serialices/deserialices todo solo para actualizar 1 campo — esa es una señal de que deberías cambiar a Hash.

4. Hash: objeto con múltiples campos

Hash es como un Map<String, String> dentro de una key, permitiendo actualizar campos individuales sin necesidad de leer/escribir todo el objeto.

@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: escribe múltiples campos a la vez, similar a HSET.
  • put un campo: solo actualiza el campo email, los demás campos (name, age…) no se afectan.
  • entries: obtiene todos los pares campo-valor del hash.

Comparación con String: si usas String + JSON, cada vez que cambies el email necesitas GET todo el JSON, parsearlo, editarlo, serializarlo de nuevo, y luego SET. Con Hash, solo un comando HSET.

Cuándo usarlo: objeto con muchos campos o actualizaciones independientes — perfil de usuario, detalle de producto, configuración por tenant.

5. List: colas e historial ordenado

List es una lista ordenada, soporta push/pop en ambos extremos. Perfecta para colas o guardar los N elementos más recientes.

@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én solo los 10 elementos más recientes
    }

    public List<String> getRecentActivities(String userId) {
        return redisTemplate.opsForList().range("activity:" + userId, 0, -1);
    }
}
  • leftPush: añade una nueva actividad al inicio de la lista.
  • trim(key, 0, 9): corta la lista manteniendo solo los primeros 10 elementos, evita que la lista crezca infinitamente.
  • range(0, -1): obtiene todos los elementos, el índice -1 significa el último elemento.

Cuándo usarlo: timeline de actividades recientes, cola de trabajos simple (combinado con BLPOP para que el consumer espere), pila de deshacer.

Nota: List no es adecuada si necesitas buscar por valor o acceso aleatorio frecuente — la complejidad de acceso por índice es O(n) con listas largas.

6. Set: conjunto sin duplicados, sin orden

Set almacena valores únicos, soporta operaciones de conjuntos (unión, intersección, diferencia) muy rápido.

@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: añade múltiples tags, Redis elimina automáticamente duplicados.
  • isMember: verifica existencia con complejidad O(1), mucho más rápido que recorrer una List.
  • intersect: encuentra tags comunes entre dos artículos — útil para sugerir artículos relacionados.

Cuándo usarlo: tags/categorías, lista de visitantes únicos por día (SADD visitors:2026-10-12 userId), verificación rápida de membresía (ej: ¿el usuario se registró para el evento?).

7. Sorted Set: ranking por puntuación

Sorted Set (ZSet) es como Set pero cada miembro tiene un score para ordenarlo. Es el tipo más potente 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 con base 0, null si no existe
        return redisTemplate.opsForZSet().reverseRank("leaderboard:weekly", playerId);
    }

    public Set<String> getTop10() {
        return redisTemplate.opsForZSet()
            .reverseRange("leaderboard:weekly", 0, 9);
    }
}
  • add(key, member, score): añade o actualiza la puntuación del jugador.
  • reverseRank: obtiene el rango en orden de puntuación decreciente (rango más alto es puntuación más alta).
  • reverseRange(0, 9): obtiene el top 10, Redis ya lo ordena automáticamente, no necesitas sort en la aplicación.

Output de ejemplo al llamar a getTop10():

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

Cuándo usarlo: tabla de clasificación, cola de prioridad (score = prioridad), rate limiting por ventana de tiempo (score = timestamp).

8. Prueba el comportamiento con JUnit 5 y Testcontainers

Para probar la lógica relacionada con Redis sin afectar datos reales, usa 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 inicia Redis real en Docker para cada test class.
  • @DynamicPropertySource sobrescribe host/puerto para que Spring Boot se conecte al contenedor correcto.
  • containsExactly verifica el orden exacto — Sorted Set garantiza que el resultado siempre esté ordenado por score decreciente.

9. Tabla de comparación rápida

Tipo Estructura Úsalo cuando
String key → value cachear object, contador, token de sesión
Hash key → mapa campo-valor object con muchos campos, actualización parcial
List key → lista ordenada cola, items recientes, timeline
Set key → conjunto sin duplicados tags, verificación de membresía, operaciones de conjuntos
Sorted Set key → miembro con score leaderboard, rate limiting, cola de prioridad

10. Errores comunes

  • Usar String para datos que se actualizan parcialmente: provoca race condition cuando múltiples threads leen-editan-escriben JSON. Cambia a Hash para actualización atómica de cada campo.
  • List sin trim: la lista crece infinitamente, consume RAM, y LRANGE se vuelve lento con el tiempo.
  • Olvidar configurar TTL: Redis no borra automáticamente las keys, los datos basura se acumulan. Siempre considera expire para datos temporales como sesiones y caché.
  • Usar Set para datos que necesitan orden: Set no garantiza orden al iterar, si necesitas ordenar usa Sorted Set.

Nota: Redis es en memoria — todos los datos están en RAM. No guardes datos grandes, de largo plazo, o datos que requieran transacciones ACID complejas (ej: saldo de cuenta bancaria) en Redis como fuente única de verdad.

11. Conclusión

Redis no es un “cachés grande”, es un conjunto de herramientas de data structure flexible. Elegir el tipo de dato correcto — String para valores simples, Hash para objetos con múltiples campos, List para colas, Set para membresía, Sorted Set para ranking — hace que el código sea más limpio, requiera menos comandos, y aproveche las operaciones atómicas de Redis en lugar de manejar esa lógica en la capa de aplicación.

rediscachingspring-bootnosqlperformance