Redis is more than a plain "key → value" string store. It provides several built-in data structures, and choosing the right one directly affects how simple your code is and how fast it runs.
The data is the same; what differs is the mechanics of the edit. With a JSON string you read the whole value, parse it, rebuild it and write it back in full; with a Hash the same edit is one command that touches one field. That is what a structure really decides — not what you can store, but how many steps and bytes an ordinary operation costs.
Why choosing the right structure matters
Imagine you store a user profile as a single JSON string. To update one field — the age — you read the whole string, parse the JSON, change the field, serialize it back, and write it again. That's five steps instead of one.
A Hash lets you update a single field directly with HSET user:42 age 31. Less code, less traffic, fewer mistakes.
String — strings and counters
String is the base Redis type. The value can be a plain string, a number, JSON, or a binary blob (for example, compressed data). The maximum size is 512 MB.
live example
SET user:42:name "Alexey"
GET user:42:name # → "Alexey"
SET page:views 0
INCR page:views # → 1
INCR page:views # → 2
INCRBY page:views 10 # → 12
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
In short: INCR / INCRBY are atomic operations with no risk of a race condition, handy for counters and rate limiters.
Typical uses of String:
- caching an HTML fragment or API response;
- a view or like counter;
- a session token or a one-time confirmation code.
TTL and key expiration
Any Redis key can be made temporary with a TTL (time to live). The key will be removed automatically once it expires.
live example
SET session:abc123 "user_data"
EXPIRE session:abc123 3600 # expires in 1 hour
# set the TTL right when writing:
SET otp:phone:79001234567 "8814" EX 300 # lives for 300 seconds
TTL session:abc123 # → seconds (-1 = no expiry, -2 = no such key)
PERSIST session:abc123 # remove the TTL, make it permanent
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Redis uses two mechanisms to evict expired keys:
- Passive expiration — the key is checked and removed the moment it is accessed.
- Active expiration — a background process periodically scans a sample of keys and removes the expired ones.
This means expired keys don't disappear instantly — they can still "live" in memory for a few more seconds until the next background pass.
A timestamp is stored next to the value, and on every read Redis compares it with the current time. The same trick in plain Java — an expired key nobody touched still takes up memory:
live example
import java.util.HashMap;
import java.util.Map;
public class ExpiryDemo {
static final Map<String, String> data = new HashMap<>();
static final Map<String, Long> deadline = new HashMap<>();
static void set(String key, String value, long ttl, long now) {
data.put(key, value);
deadline.put(key, now + ttl);
}
static String get(String key, long now) {
if (deadline.getOrDefault(key, Long.MAX_VALUE) <= now) {
data.remove(key);
deadline.remove(key);
return null;
}
return data.get(key);
}
public static void main(String[] args) {
set("session:a", "user-1", 300, 0);
set("session:b", "user-2", 300, 0);
System.out.println("second 100, session:a = " + get("session:a", 100));
System.out.println("second 400, session:a = " + get("session:a", 400));
System.out.println("keys in memory: " + data.size() + " (session:b expired, but nobody read it)");
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Hash — an object by fields
A Hash stores a set of "field → value" pairs under a single key. It's a perfect fit for objects with many attributes.
live example
HSET product:10 name "Laptop" price 89990 stock 15
HGET product:10 price # → "89990"
HGETALL product:10 # → all fields and values
HINCRBY product:10 stock -1 # decrease the stock by 1
HDEL product:10 stock # delete a single field
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Typical uses of Hash:
- a user profile (name, email, role, registration date);
- a shopping cart (product → quantity);
- application configuration.
List — queues and stacks
A List is a two-sided queue of strings. It supports adding and reading from both ends.
live example
LPUSH tasks "task-1" # add to the head
RPUSH tasks "task-2" # add to the tail
LPOP tasks # take from the head → "task-1"
RPOP tasks # take from the tail → "task-2"
LLEN tasks # list length
LRANGE tasks 0 9 # first 10 elements
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
In short: LPUSH + RPOP = a FIFO queue; LPUSH + LPOP = a LIFO stack.
The blocking variants BLPOP / BRPOP wait for an element to appear — a simple replacement for a broker in uncomplicated scenarios:
BLPOP tasks 5 # wait for an element up to 5 seconds, then return nil
Typical uses of List:
- a task queue (background jobs);
- a feed of recent events (with
LTRIMto cap the length); - a user's action history.
Set — unique elements
A Set stores an unordered collection of unique strings. Duplicates are ignored automatically.
live example
SADD tags:post:5 "java" "redis" "backend"
SADD tags:post:5 "redis" # duplicate — ignored
SMEMBERS tags:post:5 # → {"java", "redis", "backend"}
SISMEMBER tags:post:5 "java" # → 1 (present) / 0 (absent)
SCARD tags:post:5 # → 3 (set size)
# Operations over several sets:
SINTER tags:post:5 tags:post:7 # intersection
SUNION tags:post:5 tags:post:7 # union
SDIFF tags:post:5 tags:post:7 # difference
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Typical uses of Set:
- a list of unique page visitors;
- article tags;
- a list of friends or followers (intersection = mutual friends).
Sorted Set — leaderboards and ranges
A Sorted Set is like a Set, but each element carries a numeric score (weight). Elements are kept sorted by score — that's the key property.
live example
ZADD leaderboard 1500 "alice"
ZADD leaderboard 2300 "bob"
ZADD leaderboard 1800 "carol"
ZRANGE leaderboard 0 -1 WITHSCORES # all, from lowest to highest
ZREVRANGE leaderboard 0 2 # top 3, from highest to lowest
ZSCORE leaderboard "alice" # → "1500"
ZRANK leaderboard "alice" # position (0-based) ascending
ZREVRANK leaderboard "bob" # position in reverse order → 0 (1st)
ZINCRBY leaderboard 200 "alice" # add 200 to alice's score
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Since Redis 6.2 ZREVRANGE is marked deprecated in the docs: ZRANGE leaderboard 0 2 REV gives the same result.
Typical uses of Sorted Set:
- a game leaderboard (score = points);
- a priority queue (score = priority or timestamp);
- an event history with lookup by time range.
Bitmap, HyperLogLog and Streams — briefly
Bitmap — a bit array on top of a String. Each bit is addressed by an offset. It's used to record boolean facts compactly across a large number of entities.
SETBIT active_users:2024-06-01 42 1 # user 42 was active
GETBIT active_users:2024-06-01 42 # → 1
BITCOUNT active_users:2024-06-01 # number of active users that day
HyperLogLog — a probabilistic structure for counting unique elements. It takes at most 12 KB regardless of the number of elements, but gives an approximate result (error ~0.81%).
PFADD visitors:2024-06-01 "user-1" "user-2" "user-3"
PFCOUNT visitors:2024-06-01 # approximate number of unique elements
Streams — a structure for event streams, close in model to Kafka. Each entry has a unique ID and a set of fields. It supports consumer groups and read-with-acknowledgment.
XADD events * action "click" user_id "42" # add an event
XREAD COUNT 10 STREAMS events 0 # the first 10 entries from the start
XREVRANGE events + - COUNT 10 # and this way — the last 10
Streams are a good fit for an audit log, passing events between services, and a simple broker inside a single Redis.
Which structure to choose
| Task | Structure |
|---|---|
| Cache of an arbitrary value or a counter | String |
| Object with named fields | Hash |
| Queue or stack | List |
| Collection of unique values | Set |
| Leaderboard, priority queue | Sorted Set |
| Activity of millions of entities | Bitmap |
| Count of unique elements (approximate) | HyperLogLog |
| Event stream with consumer groups | Streams |
In short
- Redis provides eight built-in data structures — String, Hash, List, Set, Sorted Set, Bitmap, HyperLogLog, Streams (plus a geospatial index on top of Sorted Set).
- String works both as a string and as an atomic counter (
INCR/INCRBY). - Hash lets you update individual fields of an object without rewriting the whole value.
- List implements queues (FIFO) and stacks (LIFO);
BLPOPmakes a queue blocking. - Set stores unique elements and supports intersection, union and difference; Sorted Set adds a numeric score and keeps elements ordered.
- A key is made temporary with
EXPIRE, but an expired key releases memory later than it expires: on access, or on a background pass.
What to read next
- Redis fundamentals: how it works and why you need it — where Redis starts
- Caching patterns: cache-aside, write-through, TTL — how not to serve stale data
- Redis beyond caching: queues, locks, pub/sub — List and Streams at work
- Running Redis: persistence, memory, replication — what happens when memory runs out