Redis sorted sets and skip lists
Redis sorted sets: a skip list with random levels (p = 1/4) plus a hash table, listpacks for small sets, command costs, and memory.
What you will learn
- Keep a continuously updated ranking in a Redis sorted set.
- Explain the hash table and skip list behind a sorted set, and the listpack used for small ones.
- Know what each sorted-set command costs, in time and memory.
Why keep rankings in a sorted set
The primer serves rankings from the precomputed sales_rank table, with an index on category_id, through a memory cache. Computing a ranking live with GROUP BY … ORDER BY over a week of transactions on every read would repeat the same work tens of thousands of times a second.
If rankings are updated continuously, as in lesson 2, Redis sorted sets fit well. Redis lists leaderboards as a typical use: "you can use sorted sets to easily maintain ordered lists of the highest scores".
How Redis implements a sorted set
Redis's documentation describes sorted sets as "a dual-ported data structure containing both a skip list and a hash table":
- The hash table maps each member to its score, so
ZSCOREis . - The skip list keeps members ordered by score (ties by member), so rank and range queries are fast.
Skip lists. Every node is on the bottom level, which is an ordinary sorted linked list. Each node is also on higher levels with probability per level; Redis uses and up to 32 levels (server.h). The level is drawn at random when the node is inserted, and never depends on its score. A search starts at the top level and moves forward while the next node is smaller, then drops a level. Each Redis node also stores, per level, the span it skips, which is how ranks are computed quickly.
Small sets are different. Below zset-max-listpack-entries (128 members by default) and zset-max-listpack-value (64 bytes), Redis stores a sorted set as a compact listpack instead.
Commands and their cost (Redis docs)
| Command | What it does | Complexity |
|---|---|---|
ZADD key score member |
Insert or set a score | per member |
ZINCRBY key incr member |
Add to a score | |
ZRANGE key 0 9 REV WITHSCORES |
Top 10 | , returned |
ZREVRANK key member |
A member's rank from the top | |
ZSCORE key member |
A member's score | |
ZUNIONSTORE dest n k1 k2 … |
Sum scores across sets |
ZREVRANGE still works with the same complexity; newer code writes ZRANGE … REV.
Memory
A skip list node holds a pointer to the member string, the score, a backward pointer, and a forward pointer plus span for each level (zskiplistNode in server.h). With , a node has levels on average. Add the hash-table entry and the member string itself, and a member costs on the order of 100 bytes. Even 1,000 categories × 1,000 products is a few hundred megabytes. Measure the real figure with MEMORY USAGE rather than relying on an estimate.
A sorted set as a skip list
Members are kept in score order on level 1; each node also appears on higher levels with probability 1/4 per level, chosen at random when it is inserted. A hash map gives each member's score directly.
| L4 | head | — | — | — | — | — | — | — | — | — | — |
| L3 | head | — | — | — | — | — | — | — | — | — | — |
| L2 | head | — | — | — | 230 | — | — | — | — | — | 420 |
| L1 | head | 120 | 150 | 210 | 230 | 260 | 290 | 310 | 340 | 380 | 420 |
| Charger | Webcam | Earbuds | Tablet | Power bank | Mouse | Keyboard | Speaker | Monitor | Headphones |
- Headphones: 420
- Monitor: 380
- Speaker: 340
- Keyboard: 310
- Mouse: 290
- Read / Write Clients: 1 × Clients & Users
- Redis Proxy LB: 4 × Application Load Balancer
- Redis Primary (ZADD/ZINCRBY): 4 × Redis In-Memory Cache
- Redis Read Replicas (ZREVRANGE): 8 × Redis In-Memory Cache
- SQL sales_rank Table: 2 × Primary Relational DB
- Read / Write Clients sends requests to Redis Proxy LB
- Redis Proxy LB sends requests to Redis Primary (ZADD/ZINCRBY)
- Redis Proxy LB sends requests to Redis Read Replicas (ZREVRANGE)
- Redis Primary (ZADD/ZINCRBY) sends requests to Redis Read Replicas (ZREVRANGE)
- Redis Primary (ZADD/ZINCRBY) sends requests to SQL sales_rank Table
Key takeaways
- A sorted set pairs a hash table, so
ZSCOREis , with a skip list ordered by score, so rank queries are and range queries . - A skip list node appears on each higher level with probability 1/4, drawn at random at insert, which gives fast search.
- Sets under 128 members of up to 64 bytes are stored as a compact listpack instead.
- A precomputed
sales_ranktable read through a cache is enough when rankings change hourly.
Sources
- System Design Primer: Design Amazon's sales rank, serving the rankings
- Redis: Sorted sets
- Redis 7.4 source: zskiplistNode, ZSKIPLIST_P and ZSKIPLIST_MAXLEVEL (server.h)
- Redis: ZRANGE command
- Redis: ZUNIONSTORE command
Adapted from The System Design Primer by Donne Martin, licensed under CC BY 4.0. Changes were made.