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): retournenullsi 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.putd’un champ : met à jour uniquement le champemail, 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-1signifie 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");
}
}
@Containerinitialise un vrai Redis dans Docker pour chaque test class.@DynamicPropertySourcesurcharge le host/port pour que Spring Boot se connecte au bon container.containsExactlyvé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
trimsur la List : la liste explose, consomme de la RAM, les commandesLRANGEralentissent 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 à
expirepour 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.