← Back to the section

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.

one edit: change the profile age 30 → 31 String: the whole profile as one JSON line GET user:42 read it all age 30 → 31 parse, rebuild SET user:42 write it all the whole value travels the wire — twice Hash: the same data split into fields HSET user:42 age 31 user:42 name Alex age 30 31 city Msk one field changes — the rest are neither read nor rewritten

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 LTRIM to 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

TaskStructure
Cache of an arbitrary value or a counterString
Object with named fieldsHash
Queue or stackList
Collection of unique valuesSet
Leaderboard, priority queueSorted Set
Activity of millions of entitiesBitmap
Count of unique elements (approximate)HyperLogLog
Event stream with consumer groupsStreams

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); BLPOP makes 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.