Fondamentaux Redis : types de données et quand les utiliser

1. Aperçu général

Redis est un in-memory data store, souvent mal compris comme étant “juste un cache”. En réalité, Redis fournit plusieurs types de données (data structures) différents, chacun adapté à un groupe de problèmes spécifiques.

Cet article t’aide à maîtriser les 5 types de données les plus courants de Redis, comment les utiliser via Spring Data Redis, et plus important encore : quand choisir quel type. Une fois terminé, tu sauras éviter l’erreur classique de “tout faire avec String” et de te compliquer la vie lors de la mise à jour de données.

2. Préparation de l’environnement

Ajoute la dépendance Spring Data Redis au projet :

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

Déclare la connexion dans application.yml :

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

Lance Redis avec Docker rapidement :

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

Les exemples ci-dessous utilisent RedisTemplate<String, String> que Spring Boot configure automatiquement, tu n’as pas besoin de déclarer le bean manuellement.

3. String : le type le plus simple

String est le type basique, qui stocke une valeur associée à une clé. La valeur peut être du texte, un nombre, ou du JSON sérialisé.

@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) {
        // Sauvegarde le token avec une durée de vie de 30 minutes
        redisTemplate.opsForValue()
            .set("session:" + userId, token, Duration.ofMinutes(30));
    }

    public String getToken(String userId) {
        // Retourne null si la clé n'existe pas ou a expiré
        return redisTemplate.opsForValue().get("session:" + userId);
    }
}
  • set(key, value, ttl) : sauvegarde le token avec une durée de vie de 30 minutes, Redis l’efface automatiquement à l’expiration.
  • get(key) : retourne null si la clé n’existe pas ou a expiré.

Résultat quand on appelle getToken avec un token expiré :

null

Quand l’utiliser : session token, cache d’un objet déjà sérialisé (JSON), compteur simple (INCR), feature flag. String supporte aussi l’incrémentation atomique :

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

Attention : ne sauvegarde pas un gros objet puis sérialize/désérialize tout juste pour modifier 1 champ — c’est un signe que tu devrais passer à Hash.

4. Hash : objet avec plusieurs champs

Hash ressemble à une Map<String, String> à l’intérieur d’une clé, ce qui permet de mettre à jour chaque champ indépendamment sans lire/écrire l’objet entier.

@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) {
        // Écrit plusieurs champs en même temps
        redisTemplate.opsForHash().putAll("user:" + userId, fields);
    }

    public void updateEmail(String userId, String newEmail) {
        // Met à jour uniquement le champ email
        redisTemplate.opsForHash().put("user:" + userId, "email", newEmail);
    }

    public Map<Object, Object> getProfile(String userId) {
        return redisTemplate.opsForHash().entries("user:" + userId);
    }
}
  • putAll : écrit plusieurs champs en même temps, similaire à HSET.
  • put d’un champ : met à jour uniquement le champ email, les autres champs (name, age…) ne sont pas affectés.
  • entries : récupère tous les champs-valeurs du hash.

Comparaison avec String : avec String + JSON, à chaque changement d’email tu dois GET le JSON entier, le parser, le modifier, le sérialiser à nouveau, puis SET. Avec Hash, c’est une seule commande HSET.

Quand l’utiliser : objet avec plusieurs champs ou mises à jour indépendantes — profil utilisateur, détails produit, config par tenant.

5. List : file d’attente et historique ordonné

List est une liste ordonnée, supportant push/pop aux deux extrémités. Parfait pour une queue ou stocker les N derniers items.

@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;
        // Ajoute l'activité au début de la liste
        redisTemplate.opsForList().leftPush(key, activity);
        // Garde seulement les 10 derniers items
        redisTemplate.opsForList().trim(key, 0, 9);
    }

    public List<String> getRecentActivities(String userId) {
        return redisTemplate.opsForList().range("activity:" + userId, 0, -1);
    }
}
  • leftPush : ajoute une nouvelle activité au début de la liste.
  • trim(key, 0, 9) : coupe la liste pour garder seulement 10 éléments, évite qu’elle explose.
  • range(0, -1) : récupère tous les éléments, l’index -1 signifie le dernier élément.

Quand l’utiliser : timeline des activités récentes, simple job queue (combiné avec BLPOP pour que le consumer attende), undo stack.

Attention : List n’est pas adapté si tu dois chercher par valeur ou accéder aléatoirement — la complexité d’accès par index est O(n) avec une liste longue.

6. Set : ensemble sans doublon, sans ordre

Set stocke des valeurs uniques, supporte les opérations d’ensemble (union, intersection, différence) très rapidement.

@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) {
        // Ajoute plusieurs tags, Redis élimine automatiquement les doublons
        redisTemplate.opsForSet().add("article:tags:" + articleId, tags);
    }

    public boolean hasTag(String articleId, String tag) {
        // Vérifie si le tag existe avec une complexité O(1)
        return redisTemplate.opsForSet()
            .isMember("article:tags:" + articleId, tag);
    }

    public Set<String> commonTags(String articleId1, String articleId2) {
        // Trouve les tags communs entre deux articles
        return redisTemplate.opsForSet()
            .intersect("article:tags:" + articleId1, "article:tags:" + articleId2);
    }
}
  • add : ajoute plusieurs tags, Redis élimine automatiquement les doublons.
  • isMember : vérifie l’existence avec une complexité O(1), beaucoup plus rapide qu’une traversée de List.
  • intersect : trouve les tags communs entre deux articles — utile pour recommander des articles liés.

Quand l’utiliser : tag/catégorie, liste des visiteurs uniques par jour (SADD visitors:2026-10-05 userId), vérification d’appartenance rapide (par exemple : l’utilisateur s’est inscrit à l’événement ?).

7. Sorted Set : classement par score

Sorted Set (ZSet) ressemble à Set mais chaque member a un score pour le tri. C’est le type le plus puissant pour les problèmes 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) {
        // Ajoute ou met à jour le score du joueur
        redisTemplate.opsForZSet().add("leaderboard:weekly", playerId, score);
    }

    public Long getRank(String playerId) {
        // Rang 0-based, null si n'existe pas
        return redisTemplate.opsForZSet().reverseRank("leaderboard:weekly", playerId);
    }

    public Set<String> getTop10() {
        // Récupère le top 10, Redis l'a déjà trié
        return redisTemplate.opsForZSet()
            .reverseRange("leaderboard:weekly", 0, 9);
    }
}
  • add(key, member, score) : ajoute ou met à jour le score du joueur.
  • reverseRank : récupère le rang selon l’ordre de score décroissant (rang élevé = score élevé).
  • reverseRange(0, 9) : récupère le top 10, Redis l’a déjà trié, pas besoin de trier à l’application.

Exemple de résultat quand on appelle getTop10() :

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

Quand l’utiliser : tableau de classement, priority queue (score = priorité), rate limiting sur une fenêtre de temps (score = timestamp).

8. Tester le comportement avec JUnit 5 et Testcontainers

Pour tester la logique liée à Redis sans affecter les données réelles, utilise 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 initialise un vrai Redis dans Docker pour chaque test class.
  • @DynamicPropertySource surcharge le host/port pour que Spring Boot se connecte au bon container.
  • containsExactly vérifie l’ordre exact — Sorted Set garantit toujours un résultat trié par score décroissant.

9. Tableau comparatif rapide

Type Structure Utiliser quand
String clé → valeur cache d’objet, compteur, session token
Hash clé → map champ-valeur objet avec beaucoup de champs, mise à jour partielle
List clé → liste ordonnée queue, items récents, timeline
Set clé → ensemble sans doublon tag, vérification d’appartenance, opérations d’ensemble
Sorted Set clé → member avec score leaderboard, rate limiting, priority queue

10. Erreurs courantes

  • Utiliser String pour des données mises à jour partiellement : cela mène à des race conditions quand plusieurs threads lisent-modifient-écrivent le JSON simultanément. Passe à Hash pour une mise à jour atomique par champ.
  • Ne pas faire trim sur la List : la liste explose, consomme de la RAM, les commandes LRANGE ralentissent avec le temps.
  • Oublier de définir TTL : Redis ne supprime pas les clés par défaut, les données orphelines s’accumulent. Réfléchis toujours à expire pour les données temporaires comme session, cache.
  • Utiliser Set pour des données qui ont besoin d’ordre : Set ne garantit pas l’ordre lors de l’itération, si tu as besoin de tri utilise Sorted Set.

Attention : Redis est en mémoire — toutes les données sont dans la RAM. Ne stocke pas de données volumineuses, longue durée, ou nécessitant des transactions ACID complexes (par exemple : solde de compte bancaire) dans Redis comme unique source de vérité.

11. Conclusion

Redis n’est pas “juste un gros cache”, c’est un ensemble d’outils data structures flexibles. Choisir le bon type — String pour une valeur simple, Hash pour un objet avec plusieurs champs, List pour une queue, Set pour l’appartenance, Sorted Set pour le classement — rend ton code plus concis, moins de commandes, et tire parti des opérations atomiques de Redis au lieu de gérer cette logique à la couche application.

rediscachingspring-bootnosqlperformance