Listpack vs. Hashtable und wann Redis automatisch umschaltet
Ein Redis-Hash mit drei Feldern und ein Redis-Hash mit dreißigtausend Feldern werden intern völlig unterschiedlich gespeichert, obwohl beide über dieselben Befehle wie HSET und HGET angesprochen werden. Redis wählt automatisch zwischen einer kompakten, speicherplatzoptimierten Listpack-Kodierung für kleine Collections und einer klassischen Hashtable-Struktur für große, abhängig von konfigurierbaren Schwellenwerten. Wer diesen automatischen Umschaltpunkt kennt, kann seine Datenmodelle gezielt so gestalten, dass Redis möglichst lange in der speichereffizienten Listpack-Kodierung bleibt. Dieser Artikel zeigt, wie das Umschalten technisch funktioniert und welche praktischen Konsequenzen sich daraus für die Speicherplanung ergeben.
Inhaltsverzeichnis
- 1. Warum Redis für dieselbe Datenstruktur mehrere interne Kodierungen kennt
- 2. Das Grundprinzip von Listpack: kompakte, sequentielle Speicherung
- 3. Der Umschaltpunkt zur klassischen Hashtable-Kodierung
- 4. Standardwerte in der Praxis: 128 Elemente, 64 Byte
- 5. Speicherersparnis konkret messen: MEMORY USAGE im Vergleich
- 6. Praktische Konsequenz für die Datenmodellierung
- 7. Sonderfall Listen: Quicklist als Kette von Listpacks
- 8. Monitoring im Produktivbetrieb: Kodierungsverteilung im Blick behalten
- 9. Grenzen des Mechanismus: kein Ersatz für sinnvolle Datenmodellierung
- 10. Zusammenfassung
- 11. FAQ
1. Warum Redis für dieselbe Datenstruktur mehrere interne Kodierungen kennt
Redis-Datentypen wie Hash, List, Set und Sorted Set sind auf Befehlsebene einheitlich, intern speichert Redis sie aber je nach Größe und Inhalt in unterschiedlichen physischen Repräsentationen. Der Grund liegt im Speicherverbrauch: Eine vollwertige Hashtable mit Hash-Buckets, Zeigern und Kollisionsketten ist bei sehr vielen Einträgen effizient, weil sie konstante Zugriffszeiten unabhängig von der Elementanzahl bietet, verursacht aber pro Element einen spürbaren Speicher-Overhead durch Zeiger und interne Verwaltungsstrukturen.
Bei kleinen Collections mit wenigen Elementen überwiegt dieser Overhead den eigentlichen Nutzinhalt oft deutlich. Ein Hash mit drei kurzen Feldern würde in klassischer Hashtable-Kodierung mehr Speicher für Zeiger und Bucket-Strukturen verbrauchen als für die eigentlichen Daten. Redis begegnet diesem Problem mit einer kompakteren Alternativkodierung, die für kleine Collections den Overhead minimiert, auf Kosten linearer statt konstanter Zugriffszeit.
2. Das Grundprinzip von Listpack: kompakte, sequentielle Speicherung
Ein Listpack speichert alle Elemente einer Collection sequentiell hintereinander in einem einzigen zusammenhängenden Speicherblock, ohne separate Zeiger zwischen den Elementen. Jedes Element besteht aus einer kompakten Kodierung seiner Länge, gefolgt vom eigentlichen Wert und einem Rückwärts-Längenfeld, das es erlaubt, das Listpack auch rückwärts zu durchlaufen, ohne separate Indexstrukturen zu benötigen. Für einen Hash werden Schlüssel und Wert direkt hintereinander im selben Listpack abgelegt, für ein Sorted Set entsprechend Mitglied und Score.
Der Zugriff auf ein bestimmtes Element erfordert bei einem Listpack einen linearen Scan vom Anfang des Blocks, da es keine wahlfreien Zugriffsstrukturen gibt. Bei einer kleinen Anzahl an Elementen fällt dieser lineare Scan in der Praxis kaum ins Gewicht, weil moderne CPUs sequentiellen Speicherzugriff durch Cache-Lokalität sehr effizient verarbeiten, oft schneller als das Verfolgen mehrerer verstreuter Zeiger bei einer Hashtable mit vergleichbar wenigen Elementen.
# Kleinen Hash anlegen und Kodierung pruefen
HSET user:100 name Alice age 30 city Berlin
OBJECT ENCODING user:100
# -> "listpack"
# Kleines Sorted Set anlegen und Kodierung pruefen
ZADD leaderboard:daily 100 user:1 95 user:2 80 user:3
OBJECT ENCODING leaderboard:daily
# -> "listpack"
3. Der Umschaltpunkt zur klassischen Hashtable-Kodierung
Sobald eine Collection eine der konfigurierten Schwellenwerte überschreitet, entweder die maximale Elementanzahl oder die maximale Größe eines einzelnen Elements, wandelt Redis die Kodierung automatisch und dauerhaft in die klassische Hashtable-Kodierung um. Diese Umwandlung ist einseitig: Ein Hash, der einmal zur Hashtable-Kodierung gewechselt hat, wechselt auch dann nicht zurück zu Listpack, wenn nachträglich Elemente gelöscht werden und die Collection wieder unter die Schwellenwerte schrumpft. Redis optimiert bewusst nicht für diesen Rückwärtsfall, um die Kodierungslogik einfach zu halten und unnötige Umwandlungsoperationen bei schwankender Elementanzahl zu vermeiden.
Für Hashes steuern hash-max-listpack-entries und hash-max-listpack-value diesen Schwellenwert, für Sets set-max-listpack-entries und set-max-listpack-value, für Sorted Sets zset-max-listpack-entries und zset-max-listpack-value, und für Listen list-max-listpack-size. Sobald ein Element länger als der konfigurierte Wert für die maximale Elementgröße ist, etwa ein einzelner Feldwert eines Hashes, wandelt Redis die Kodierung um, selbst wenn die Elementanzahl insgesamt noch klein ist.
# Aktuelle Schwellenwerte für Hash-Kodierung anzeigen
CONFIG GET hash-max-listpack-entries
CONFIG GET hash-max-listpack-value
# Schwellenwerte anpassen (in redis.conf oder zur Laufzeit)
CONFIG SET hash-max-listpack-entries 128
CONFIG SET hash-max-listpack-value 64
4. Standardwerte in der Praxis: 128 Elemente, 64 Byte
In der Standardkonfiguration liegt der Schwellenwert für die Elementanzahl bei Hashes, Sets und Sorted Sets üblicherweise bei 128 Einträgen, der Schwellenwert für die maximale Größe eines einzelnen Elements bei 64 Byte. Diese Werte sind bewusst konservativ gewählt, um in typischen Anwendungsfällen einen guten Kompromiss zwischen Speicherersparnis und Zugriffsgeschwindigkeit zu bieten, ohne dass Anwender die Konfiguration überhaupt anfassen müssen.
Für Anwendungsfälle mit sehr vielen kleinen, aber überschaubar großen Collections lohnt es sich, diese Standardwerte gezielt zu überprüfen. Wird beispielsweise ein Hash mit typischerweise 150 kurzen Feldern verwendet, verpasst man mit dem Standardwert von 128 knapp die Listpack-Kodierung, obwohl die Collection inhaltlich noch klein genug wäre, um von den Speichervorteilen zu profitieren. Eine moderate Anhebung auf 200 könnte in diesem Fall spürbare Speicherersparnis bringen, ohne die Zugriffsgeschwindigkeit in der Praxis relevant zu verschlechtern.
5. Speicherersparnis konkret messen: MEMORY USAGE im Vergleich
Der tatsächliche Speicherunterschied zwischen Listpack- und Hashtable-Kodierung lässt sich mit dem Befehl MEMORY USAGE direkt messen, indem man dieselbe logische Datenstruktur einmal knapp unter und einmal knapp über dem konfigurierten Schwellenwert anlegt und den gemeldeten Speicherverbrauch vergleicht. In der Praxis zeigen sich für kleine Hashes häufig Einsparungen von deutlich mehr als der Hälfte gegenüber der klassischen Hashtable-Kodierung, abhängig von der Feldanzahl und der Länge der gespeicherten Werte.
Diese Messung sollte vor einer geplanten Anpassung der Schwellenwerte immer mit realistischen, aus der eigenen Anwendung stammenden Beispieldaten erfolgen, da die tatsächliche Einsparung stark von der Feldnamenlänge, der Wertlänge und der Elementanzahl der konkreten Collection abhängt und sich nicht pauschal auf jede Anwendung übertragen lässt.
# Speicherverbrauch eines Hashes direkt messen
HSET session:abc token xyz789 user_id 42 role admin
MEMORY USAGE session:abc
# -> (integer) 96 (Listpack-Kodierung)
# Denselben Hash kuenstlich über den Schwellenwert vergroessern
CONFIG SET hash-max-listpack-entries 2
HSET session:abc extra_field value
OBJECT ENCODING session:abc
# -> "hashtable"
MEMORY USAGE session:abc
# -> deutlich höherer Wert durch Hashtable-Overhead
6. Praktische Konsequenz für die Datenmodellierung
Die zentrale praktische Konsequenz aus dem Listpack-Mechanismus ist, dass viele kleine Collections speichereffizienter sind als wenige große, sofern die Zugriffsmuster das erlauben. Statt eines einzigen riesigen Hashes mit hunderttausenden Feldern, der zwangsläufig als Hashtable kodiert wird, kann eine Aufteilung in viele kleinere, thematisch gruppierte Hashes, die jeweils unter dem Schwellenwert bleiben, in Summe deutlich weniger Speicher verbrauchen, etwa durch Sharding nach Kategorie oder Zeitfenster statt eines globalen Sammel-Hashes.
Für ein Magento-Setup betrifft das etwa Session-Daten, Produktattribut-Caches oder Konfigurationswerte pro Shop-Store: Wird pro logischer Einheit, etwa pro Session oder pro Produkt, ein eigener, kleiner Hash verwendet, bleibt dieser in aller Regel deutlich unter den Standardschwellenwerten und profitiert dauerhaft von der Listpack-Kodierung, während ein einziger globaler Hash über alle Sessions hinweg zwangsläufig zur teureren Hashtable-Kodierung wechseln würde.
7. Sonderfall Listen: Quicklist als Kette von Listpacks
Bei Redis-Listen funktioniert die Kodierungslogik etwas anders als bei Hashes, Sets und Sorted Sets. Eine kleine Liste wird ebenfalls als einzelnes Listpack gespeichert, wächst die Liste jedoch über den konfigurierten Schwellenwert list-max-listpack-size hinaus, wechselt Redis nicht direkt zu einer klassischen verketteten Liste, sondern zu einer Quicklist, einer verketteten Struktur, deren einzelne Knoten selbst wiederum Listpacks sind.
Dieser Zwischenschritt kombiniert die Vorteile beider Ansätze: Innerhalb eines einzelnen Quicklist-Knotens bleibt der Speicher-Overhead durch die kompakte Listpack-Kodierung gering, während die Verkettung mehrerer Knoten das Wachstum der Liste über beliebig viele Elemente hinweg erlaubt, ohne dass ein einzelner Speicherblock unbegrenzt groß werden müsste. Für sehr lange Listen, etwa Warteschlangen mit tausenden Einträgen, ist dieses Verhalten weitgehend transparent und erfordert keine gesonderte Behandlung im Anwendungscode.
8. Monitoring im Produktivbetrieb: Kodierungsverteilung im Blick behalten
Für ein laufendes System lohnt sich gelegentliches Monitoring, welcher Anteil der Schlüssel eines bestimmten Präfixes tatsächlich noch in Listpack-Kodierung vorliegt. Da OBJECT ENCODING nur für einen einzelnen Schlüssel auf einmal funktioniert, empfiehlt sich für eine aggregierte Übersicht ein kleines Skript, das über SCAN alle Schlüssel eines Präfixes durchläuft und die Kodierung jedes einzelnen Schlüssels sammelt, statt KEYS zu verwenden, das den Server bei großen Datenmengen blockieren kann.
Ein plötzlicher Anstieg des Anteils an Hashtable-kodierten Schlüsseln innerhalb einer Datenklasse, die eigentlich klein bleiben sollte, ist oft ein früher Indikator für ein sich änderndes Nutzungsmuster in der Anwendung, etwa wachsende Produktattribut-Listen oder eine unerwartet hohe Anzahl an Session-Feldern, und sollte als Anlass genommen werden, sowohl die Datenmodellierung als auch die konfigurierten Schwellenwerte erneut zu überprüfen.
#!/bin/bash
# Kodierungsverteilung für alle Schlüssel mit Praefix "session:" ermitteln
redis-cli --scan --pattern 'session:*' | while read -r key; do
redis-cli OBJECT ENCODING "$key"
done | sort | uniq -c
9. Grenzen des Mechanismus: kein Ersatz für sinnvolle Datenmodellierung
So nützlich die automatische Listpack-Kodierung für die Speicherplanung ist, sie ersetzt keine durchdachte Datenmodellierung. Ein Datenmodell, das ohnehin sehr viele Elemente pro Schlüssel erfordert, etwa eine globale Produktdatenbank mit hunderttausenden Einträgen in einem einzigen Sorted Set, profitiert von der Kodierungsschwelle nicht, weil die Hashtable-Kodierung hier ohnehin unvermeidlich und inhaltlich auch sinnvoll ist, da sie konstante statt linearer Zugriffszeit bei großen Elementmengen bietet.
Die Kodierungsschwelle ist deshalb vor allem als Optimierungshebel für Collections zu verstehen, deren Größe man ohnehin bewusst klein hält oder klein halten kann, nicht als Grund, künstlich kleine Collections zu erzwingen, wo eine größere, zusammenhängende Struktur inhaltlich die bessere Wahl wäre. Die Entscheidung zwischen vielen kleinen und wenigen großen Collections sollte primär vom Zugriffsmuster der Anwendung geleitet werden, die Speicherkodierung ist ein sekundärer, aber messbar wirksamer Optimierungsfaktor.
| Datentyp | Konfigurationsparameter (Anzahl) | Konfigurationsparameter (Größe) | Standardwert |
|---|---|---|---|
| Hash | hash-max-listpack-entries | hash-max-listpack-value | 128 Einträge / 64 Byte |
| Set | set-max-listpack-entries | set-max-listpack-value | 128 Einträge / 64 Byte |
| Sorted Set | zset-max-listpack-entries | zset-max-listpack-value | 128 Einträge / 64 Byte |
| Liste | list-max-listpack-size | (kombiniert mit Knotenanzahl) | 128 Einträge pro Quicklist-Knoten |
| Rückumwandlung | Nicht vorhanden | Nicht vorhanden | Einmal Hashtable, immer Hashtable |
Mironsoft
Cache-Layer-Setup und Magento-Redis-Integration
Magento-Cache, der nicht richtig greift oder falsch konfiguriert ist?
Wir richten Redis als Cache- und Session-Backend für Magento sauber ein, tunen Speicherverbrauch und Eviction-Strategien und sorgen dafür, dass Full Page Cache und Session-Storage zuverlässig zusammenspielen.
Redis-Setup
Cache-, Session- und FPC-Backend produktionsreif für Magento konfigurieren.
Memory-Tuning
Speicherverbrauch und Eviction-Policies auf die tatsächliche Shop-Last abstimmen.
High-Availability-Setup
Redis Sentinel oder Cluster für ausfallsichere Magento-Umgebungen einrichten.
10. Zusammenfassung
OBJECT ENCODING verstehen: Das Wichtigste auf einen Blick
Grundprinzip
Kleine Collections speichert Redis als kompaktes, sequentielles Listpack ohne Zeiger-Overhead, große als klassische Hashtable mit konstanter Zugriffszeit.
Umschaltpunkt
Konfigurierbare Schwellenwerte für Elementanzahl und maximale Elementgröße steuern den Wechsel, standardmäßig 128 Einträge beziehungsweise 64 Byte.
Einseitigkeit
Der Wechsel zur Hashtable-Kodierung ist dauerhaft, ein Schrumpfen der Collection führt nicht zurück zu Listpack.
Praxiskonsequenz
Viele kleine, thematisch getrennte Collections statt weniger großer Sammelstrukturen nutzen die Speichervorteile von Listpack gezielt aus.