Level 1 · 20 min
Redis Data Structures
Redis is not just a key-value store — it's a data structure server. Choosing the right data structure is crucial for performance and memory efficiency. Each structure is optimized for specific access patterns.
Strings and Hashes
Strings use raw encoding for values >44 bytes, embstr for shorter values (single allocation fitting in a 64-byte CPU cache line). INCR is atomic because Redis is single-threaded — no compare-and-swap needed. Hashes use listpack encoding when both field count <128 AND all values ≤64 bytes — a contiguous byte array that packs key-value pairs sequentially, using roughly 10× less memory than a hashtable. Once either threshold is crossed, Redis auto-promotes to hashtable. Verify with: OBJECT ENCODING user:1001. Fields are stored in insertion order inside listpack, giving O(N) field lookup — but with <128 fields, linear scan beats hashtable due to CPU cache locality. HRANDFIELD (Redis 6.2) returns random fields without loading all fields — useful for sampling.
Lists and Sets
Lists use listpack when count <128 AND each element ≤64 bytes, otherwise quicklist (a doubly-linked list of listpack nodes). Node size is governed by list-max-listpack-size (default 128 entries per node). BLPOP blocks the connection at the Redis event-loop level — no busy-polling, zero CPU while waiting. Sets use intset (sorted integer array, O(log N) binary search) when all members are integers and count <512; otherwise hashtable. SINTERCARD (Redis 7.0) returns cardinality of intersection without materializing full member lists — critical for large set intersection checks. HyperLogLog uses the Flajolet-Martin algorithm with 16384 registers, consuming ≤12 KB, and guarantees ≤0.81% standard error regardless of cardinality — verified empirically at 100M+ distinct values. PFMERGE combines HLL counters linearly, enabling daily→weekly→monthly unique-visitor rollups with constant memory per time bucket. Production insight from Redis in Action: the ziplist representation for short Lists, Hashes, and Sorted Sets stores a serialized sequence of length-length-string entries — it must be fully decoded for every read and partially re-encoded for every write, which is why performance degrades non-linearly once you cross the element count or size threshold. Carlson documents that on a 32-bit platform, three 3-character strings in a doubly-linked list consume 21 bytes of overhead for 3 bytes of actual data, while the ziplist encoding stores the same data with near-zero overhead. Verify the actual encoding any time you suspect a threshold crossing: OBJECT ENCODING mykey returns 'listpack', 'quicklist', 'intset', or 'hashtable' depending on the current internal representation.
Sorted Sets and Streams
Sorted Sets use dual encoding: listpack when count <128 AND all members ≤64 bytes, otherwise a skiplist + hashtable combination. The skiplist provides O(log N) for ZADD/ZRANK/ZRANGE; the parallel hashtable gives O(1) ZSCORE lookup. Memory cliff: crossing the 128-element threshold causes roughly 6× memory increase — keep this in mind when designing leaderboard key schemas. Streams use a radix tree of listpack nodes. Each node holds up to stream-node-max-entries entries (default 100). The PEL (Pending Entry List) tracks messages delivered to a consumer group but not yet ACKed — XPENDING shows PEL contents, XCLAIM reassigns stale messages after consumer crash, XAUTOCLAIM (Redis 7.0) does both atomically. RESP3 protocol (Redis 6+) supports server-push notifications, enabling true event-driven subscription without RESP2's requirement to block on a dedicated read loop.
Code example
# Leaderboard using Sorted Set
ZADD leaderboard 1500 "player:alice"
ZADD leaderboard 2300 "player:bob"
ZADD leaderboard 1800 "player:charlie"
# Top 3 (descending)
ZREVRANGE leaderboard 0 2 WITHSCORES
# → player:bob 2300, player:charlie 1800, player:alice 1500
# User profile using Hash
HSET user:1001 name "Alice" email "alice@example.com" level "gold"
HGET user:1001 name # → Alice
HGETALL user:1001 # → all fields
# Work queue using List
LPUSH tasks:email {"to":"alice","subject":"Welcome"}
BRPOP tasks:email 30 # blocks up to 30s waiting for a task