RizTech Academy logo
RizTech Academy
Key-Value Stores and CachingLesson 2 of 640 min

Strings, hashes, lists, sets and sorted sets

Most caches store one thing: an opaque blob under a key. Redis stores typed data structures — strings, hashes, lists, sets, sorted sets — each with atomic operations that run server-side. This is what makes Redis more than a cache, and this lesson is the five you will use, with the commands run against a real Redis 7 so the output is exactly what you will see.

Everything is still a key pointing at a value. What changes is what the value is, and what you can do to it in one atomic step.

Strings — the workhorse

The value is a string (or a number, which is a string Redis can do arithmetic on):

SET book:1:title "The Long Monsoon"      → OK
GET book:1:title                          → "The Long Monsoon"
INCR book:1:views                         → 1
INCR book:1:views                         → 2

INCR is the one to notice: it increments atomically on the server, with no read-modify-write race. Two clients incrementing a view counter at once cannot lose a count — the same lost-update safety as PostgreSQL's UPDATE ... SET n = n + 1, but at Redis speed. INCRBY, DECR and INCRBYFLOAT round it out.

The naming convention — book:1:title, member:42:name — is not enforced; Redis has no schema. But colon-separated keys are the universal Redis idiom, and they keep an otherwise flat keyspace navigable. Adopt it.

Strings are where you cache serialised objects too: SET book:1 '{"title":"...","copies":3}' with a JSON blob is the simplest cache-a-row pattern, and often all you need.

Hashes — a record under one key

When you want fields, not a blob:

HSET member:1 name "Kavita Joshi" membership senior    → 2
HGETALL member:1
  1) "name"
  2) "Kavita Joshi"
  3) "membership"
  4) "senior"

A hash is a map of field→value under a single key — a row, essentially. HGET member:1 name reads one field without deserialising the whole thing, and HINCRBY member:1 loans 1 increments one numeric field atomically. Prefer a hash over a JSON string when you update individual fields, and a JSON string when you always read and write the whole object.

Lists — ordered, and a queue

A list is an ordered sequence, fast to push and pop at either end:

RPUSH queue:emails "a@x.com" "b@x.com"     → 2        (append right)
LPUSH queue:emails "urgent@x.com"          → 3        (prepend left)
LRANGE queue:emails 0 -1
  1) "urgent@x.com"
  2) "a@x.com"
  3) "b@x.com"
LPOP queue:emails                          → "urgent@x.com"

Push on one end, pop from the other and you have a queue — the basis of a simple job queue. BLPOP blocks until an item arrives, so a worker can wait efficiently rather than polling. -1 as an index means "the last element", the same convention as Python. Keep lists bounded (LTRIM) or they grow forever — the unbounded-collection trap, again.

Sets — membership and uniqueness

An unordered collection of unique members:

SADD book:1:tags fiction literary fiction  → 2        (the duplicate is ignored)
SMEMBERS book:1:tags                         → "fiction", "literary"
SCARD book:1:tags                            → 2       (count)
SISMEMBER book:1:tags fiction                → 1       (membership test, O(1))

Adding fiction twice added it once — uniqueness is free. SISMEMBER is an instant membership test, which is the point: "has this user seen this article", "is this IP blocked". And sets do set algebra on the server — SINTER (intersection), SUNION, SDIFF — so "users who follow both A and B" is one command. SADD on a "unique visitors today" set is a cheap approximate-free way to count distinct things.

Sorted sets — the one that surprises people

A set where every member carries a score, and members are kept ordered by that score. This is the structure that makes Redis famous, because a leaderboard is a one-liner:

ZADD leaderboard 100 kavita 250 ravi 175 neha    → 3
ZREVRANGE leaderboard 0 -1 WITHSCORES
  1) "ravi"    250
  2) "neha"    175
  3) "kavita"  100
ZRANK leaderboard ravi                            → returns rank ascending
ZINCRBY leaderboard 50 kavita                     → 150   (bump a score atomically)

Ranked, top-N, "give me positions 10–20", "what rank is this user" — all fast, all maintained automatically as scores change. The uses go well beyond games: a priority queue (score = priority), a time-ordered feed (score = timestamp), rate limiting (score = request time), and range queries (ZRANGEBYSCORE for "everyone scoring 100–200"). When you need "sorted by a number, updated live", this is the structure.

Keys, expiry and inspection

Operations on keys themselves, which apply whatever the value type:

EXISTS book:1                → 1
DEL book:1                   → 1
TYPE member:1                → hash
EXPIRE session:abc 3600      → 1        (delete after 3600 seconds)
TTL session:abc              → 3600     (seconds left; -1 = no expiry, -2 = gone)
SET session:abc "u42" EX 100             (set with expiry in one atomic step)

EXPIRE and TTLs are central to using Redis as a cache — they are the whole next-but-one lesson — and setting the value and its expiry together (SET ... EX) avoids a window where the key exists forever because the process died between the two commands.

One warning: never run KEYS * on a production server. It scans the entire keyspace and blocks Redis — which is single-threaded — for as long as it takes, freezing every other client. Use SCAN, which returns keys in small batches without blocking. This is the single most common way people accidentally take Redis down.

Beyond the five

Redis has more when you need it: streams (an append-only log, for event pipelines — module 12 touches this), bitmaps and HyperLogLog (count distinct things in tiny fixed memory — "unique visitors" in 12 KB regardless of count), geospatial indexes, and pub/sub for messaging. You will not reach for these often, but knowing they exist stops you rebuilding them.

Atomicity and doing several things at once

Every single command above is atomic — Redis runs one command at a time. For several commands as a unit, MULTI/EXEC groups them into a transaction (though without the rollback semantics of SQL), and a Lua script (EVAL) runs a whole block atomically on the server, which is how the correct rate limiter in a later lesson is built. The single-threaded model that makes Redis fast is also what makes this atomicity simple.

Check your work

What makes Redis more than a plain cache. Typed data structures with atomic server-side operations.

What is still constant across all of them. A key pointing at a value.

Why INCR matters. It increments atomically, immune to the lost-update race.

The key-naming idiom. Colon-separated (book:1:title) — unenforced but universal.

Hash versus JSON string. Hash when you update individual fields; JSON string when you read and write the whole object.

How a list becomes a queue. Push one end, pop the other; BLPOP blocks for a worker.

What a set gives you for free. Uniqueness, O(1) membership tests, and server-side set algebra.

What a sorted set adds. A score per member, kept ordered — leaderboards, priority queues, time-ordered feeds, range-by-score.

Setting a value and expiry together, and why. SET ... EX — so a crash between two commands cannot leave the key without an expiry.

The command that can take Redis down. KEYS * — it blocks the single thread; use SCAN.

How to run several commands atomically. MULTI/EXEC, or a Lua script with EVAL.

Practice

  1. SET a title and GET it back. Then INCR a counter twice and confirm it is 2.
  2. Have two shells INCR the same key many times and confirm no increments are lost.
  3. Store a member as a hash and read one field with HGET. Then HINCRBY a numeric field.
  4. Store the same member as a JSON string. List when each representation is better.
  5. Build a queue with RPUSH/LPOP. Then try BLPOP in one shell and RPUSH in another.
  6. Add a duplicate to a set and confirm it appears once. Test membership with SISMEMBER.
  7. Build two sets and compute their intersection with SINTER.
  8. Build a leaderboard with ZADD, read the top 3 with ZREVRANGE, and bump one score with ZINCRBY.
  9. Use a sorted set as a time-ordered feed: score = timestamp, then ZREVRANGE for "most recent".
  10. Set a key with EXPIRE, watch TTL count down, and confirm it disappears.
  11. Set a value and its expiry in one SET ... EX and explain the race it avoids.
  12. Load a few thousand keys, then compare KEYS * and SCAN. (On a toy instance only — never on production.)

Official documentation

Next: the caching patterns, and the hard problem of invalidation.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship