Redis Basics: Datentypen und wann man sie einsetzt

1. Übersicht

Redis ist ein In-Memory-Datenspeicher, der oft als bloße „Cache-Lösung” missverstanden wird. Tatsächlich bietet Redis viele verschiedene Datentypen (Data Structures), von denen jeder für eine bestimmte Gruppe von Problemen geeignet ist.

Dieser Artikel hilft dir, die 5 häufigsten Redis-Datentypen zu verstehen, wie man sie mit Spring Data Redis verwendet, und vor allem: wann man welchen Typ wählt. Nach dem Lesen wirst du den klassischen Fehler vermeiden, „String für alles” zu verwenden, und dich nicht selbst Kopfschmerzen bereiten, wenn du Daten aktualisierst.

2. Umgebung vorbereiten

Füge die Spring Data Redis Abhängigkeit zu deinem Projekt hinzu:

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

Deklariere die Verbindung in application.yml:

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

Starte Redis schnell mit Docker:

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

Die folgenden Beispiele verwenden RedisTemplate<String, String>, das Spring Boot automatisch konfiguriert, du brauchst also keinen Bean manuell zu deklarieren.

3. String: der einfachste Typ

String ist der grundlegende Typ, der einen Wert an einen Schlüssel bindet. Der Wert kann Text, eine Zahl oder serialisiertes JSON sein.

@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): Token speichern mit Gültigkeitsdauer von 30 Minuten, Redis löscht es automatisch nach Ablauf.
  • get(key): gibt null zurück, wenn der Schlüssel nicht vorhanden ist oder abgelaufen ist.

Ausgabe beim Aufrufen von getToken mit abgelaufenem Token:

null

Wann man es verwendet: Session-Token, Caching eines bereits serialisierten Objekts (JSON), einfache Zähler (INCR), Feature Flags. String unterstützt auch atomare Inkremente:

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

Hinweis: speichere ein großes Objekt nicht und serialisiere/deserialisiere es komplett nur für eine Feldaktualisierung — das ist ein Zeichen dafür, dass du zu Hash wechseln solltest.

4. Hash: Objekt mit mehreren Feldern

Hash ist wie eine Map<String, String> innerhalb eines Schlüssels und ermöglicht es, einzelne Felder zu aktualisieren, ohne das gesamte Objekt lesen/schreiben zu müssen.

@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: mehrere Felder gleichzeitig schreiben, ähnlich wie HSET.
  • put ein Feld: aktualisiert nur das Feld email, andere Felder (name, age…) sind nicht betroffen.
  • entries: holt alle Feld-Wert-Paare des Hash.

Vergleich mit String: Wenn du String + JSON verwendest, musst du jedes Mal, wenn du eine E-Mail änderst, das gesamte JSON GETen, parsen, ändern, erneut serialisieren und dann SETen. Mit Hash brauchst du nur einen HSET-Befehl.

Wann man es verwendet: Objekte mit mehreren Feldern oder unabhängigen Aktualisierungen — Benutzerprofil, Produktdetails, Konfiguration pro Mandant.

5. List: Warteschlange und chronologischer Verlauf

List ist eine geordnete Liste, die Push/Pop an beiden Enden unterstützt. Ideal für Warteschlangen oder zum Speichern der N neuesten Elemente.

@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); // behält die 10 neuesten Elemente
    }

    public List<String> getRecentActivities(String userId) {
        return redisTemplate.opsForList().range("activity:" + userId, 0, -1);
    }
}
  • leftPush: fügt neue Aktivität am Anfang der Liste hinzu.
  • trim(key, 0, 9): schneidet die Liste auf 10 Elemente zu, verhindert unbegrenztes Wachstum.
  • range(0, -1): holt alle Elemente, Index -1 bedeutet das letzte Element.

Wann man es verwendet: neueste Aktivitäts-Timeline, einfache Job-Warteschlange (mit BLPOP zum Warten von Consumern), Undo-Stack.

Hinweis: List ist nicht geeignet, wenn du nach Wert suchen musst oder häufig zufällig auf Elemente zugreifen musst — die zeitliche Komplexität des Index-Zugriffs ist O(n) für lange Listen.

6. Set: Eindeutige Sammlung ohne Reihenfolge

Set speichert eindeutige Werte und unterstützt Mengenoperationen (Union, Schnitt, Differenz) sehr schnell.

@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: fügt mehrere Tags hinzu, Redis entfernt Duplikate automatisch.
  • isMember: prüft Existenz in O(1) Zeit, viel schneller als das Durchsuchen einer Liste.
  • intersect: findet gemeinsame Tags zwischen zwei Artikeln — verwendet für verwandte Artikel-Empfehlungen.

Wann man es verwendet: Tags/Kategorien, Liste eindeutiger Besucher pro Tag (SADD visitors:2026-10-05 userId), schnelle Membership-Prüfung (z.B. hat ein Benutzer das Event registriert).

7. Sorted Set: Ranglisten nach Punktzahl

Sorted Set (ZSet) ist wie Set, aber jedes Mitglied hat einen score zum Sortieren. Das ist der mächtigste Typ für Leaderboards.

@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) {
        // Rang ist 0-basiert, null wenn nicht vorhanden
        return redisTemplate.opsForZSet().reverseRank("leaderboard:weekly", playerId);
    }

    public Set<String> getTop10() {
        return redisTemplate.opsForZSet()
            .reverseRange("leaderboard:weekly", 0, 9);
    }
}
  • add(key, member, score): fügt einen Spieler hinzu oder aktualisiert seine Punktzahl.
  • reverseRank: holt den Rang in absteigender Punktzahl (höchste Punktzahl = höchster Rang).
  • reverseRange(0, 9): holt top 10, Redis sortiert bereits automatisch, keine Sortierung in der Anwendung nötig.

Beispielausgabe beim Aufrufen von getTop10():

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

Wann man es verwendet: Leaderboards, Priority Queues (score = Priorität), Rate Limiting nach Zeitfenster (score = Zeitstempel).

8. Verhalten mit JUnit 5 und Testcontainers testen

Um Redis-bezogene Logik zu testen, ohne echte Daten zu beeinflussen, verwende 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 initialisiert echtes Redis in Docker für jede Test-Klasse.
  • @DynamicPropertySource überschreibt Host/Port, damit Spring Boot sich mit dem richtigen Container verbindet.
  • containsExactly prüft die genaue Reihenfolge — Sorted Set garantiert, dass Ergebnisse immer nach absteigender Punktzahl sortiert sind.

9. Schnellvergleichstabelle

Typ Struktur Verwende wenn
String key → value Objekt-Caching, Zähler, Session-Token
Hash key → field-value map Objekt mit mehreren Feldern, teilweise Aktualisierungen
List key → geordnete Liste Warteschlange, aktuelle Elemente, Timeline
Set key → Sammlung ohne Duplikate Tags, Membership-Prüfung, Mengenoperationen
Sorted Set key → member mit score Leaderboard, Rate Limiting, Priority Queue

10. Häufige Fehler

  • String für Daten verwenden, die häufig teilweise aktualisiert werden: führt zu Race Conditions, wenn mehrere Threads gleichzeitig JSON lesen-ändern-schreiben. Wechseln Sie zu Hash für atomare Feld-Updates.
  • List nicht trimmen: Liste wächst unbegrenzt, verbraucht RAM, LRANGE wird mit der Zeit langsamer.
  • TTL vergessen zu setzen: Redis löscht Keys standardmäßig nicht automatisch, Garbage Data sammelt sich an. Setze immer expire für temporäre Daten wie Sessions und Cache.
  • Set für Daten verwenden, die Reihenfolge brauchen: Set garantiert keine Iterationsreihenfolge, verwende Sorted Set wenn du sortiert brauchst.

Hinweis: Redis ist In-Memory — alle Daten befinden sich im RAM. Speichern Sie keine großen, langfristigen oder ACID-Transaktionen brauchenden Daten (z.B. Kontosaldo) in Redis als einzige Quelle der Wahrheit (Source of Truth).

11. Fazit

Redis ist nicht einfach ein „großer Cache”, sondern eine flexible Sammlung von Datenstruktur-Tools. Wähle den richtigen Datentyp — String für einzelne Werte, Hash für Objekte mit mehreren Feldern, List für Warteschlangen, Set für Membership, Sorted Set für Ranglisten — macht deinen Code kompakter, benötigt weniger Befehle und nutzt die Stärke von atomaren Operationen in Redis, anstatt die Logik selbst auf der Anwendungsseite zu implementieren.

rediscachingspring-bootnosqlperformance