Unlocking Redis: 9 Real‑World Patterns from KV Cache to Bloom Filters

This article walks through nine practical Redis use cases—including basic KV caching, distributed locking, delayed queues, rate limiting, service discovery, bitmap storage, HyperLogLog counting, roaring bitmaps, and Bloom filters—explaining the underlying concepts, configuration tips, and code examples for robust backend development.

Java Backend Technology
Java Backend Technology
Java Backend Technology
Unlocking Redis: 9 Real‑World Patterns from KV Cache to Bloom Filters

Business Background

The author shares nine classic Redis scenarios encountered in daily backend development, aiming to help engineers apply Redis's advanced features effectively.

KV Cache

Redis is used to cache user, session, and product data. Setting an appropriate expiration time aligns with a user's session length and prevents stale data.

def get_user(user_id):
    user = redis.get(user_id)
    if not user:
        user = db.get(user_id)
        redis.setex(user_id, ttl, user)  # set expiration
    return user

def save_user(user):
    redis.setex(user.id, ttl, user)  # set expiration
    db.save_async(user)  # async write to DB

When memory usage grows, scaling is achieved with Codis or Redis‑Cluster.

Redis provides several eviction policies. The author’s production uses allkeys‑lru , which evicts any key based on recent usage, while volatile‑lru only evicts keys with an expiration.

Distributed Lock

A simple lock is implemented with SET key value NX EX ttl. The lock must store an owner_id to ensure only the lock holder can release it, avoiding deadlocks caused by process crashes.

# acquire lock
set lock:$user_id owner_id nx ex=5
# release lock (Lua script)
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

Although Redis’ official recommendation is RedLock, the author prefers the simple lock for lower operational cost.

Delayed Queue

When a lock conflict occurs, the task is pushed to a delayed queue implemented with a sorted set (zset) where the score is the future execution timestamp.

# produce delayed message
zadd queue-key now_ts+5 task_json
# consume delayed message
while True:
    task_json = zrevrangebyscore(queue-key, now_ts, 0, 0, 1)
    if task_json:
        if zrem(queue-key, task_json):
            process_task(task_json)
    else:
        sleep(1)

To avoid race conditions in multi‑threaded consumers, the polling and removal are wrapped in a Lua script for atomicity.

Rate Limiting

Rate limiting is achieved by storing each user’s actions in a zset, trimming entries older than the time window, and counting the remaining members.

hist_key = "ugc:${user_id}"
with redis.pipeline() as pipe:
    pipe.zadd(hist_key, ts, uuid)
    pipe.zremrangebyscore(hist_key, 0, now_ts-3600)
    pipe.zcard(hist_key)
    pipe.expire(hist_key, 3600)
    _, _, count, _ = pipe.execute()
    return count > N

Service Discovery

Services register their heartbeat in a zset ( zadd service_key heartbeat_ts address). Consumers poll the zset and clean up entries that haven’t refreshed within a timeout.

zadd service_key heartbeat_ts addr
zrem service_key addr
zremrangebyscore service_key 0 now_ts-30  # remove stale entries

A version key is incremented on any change, allowing clients to poll a global version and reload only affected service lists.

Bitmap for Sign‑In

Initially, sign‑in data was stored in hashes, which consumed excessive memory. Switching to a bitmap reduced storage to a few bytes per user per month.

# set sign‑in bits (2 bits per day)
setbit sign:${user_id} ${offset} ${value}

For very sparse large bitmaps, a roaring bitmap (segment‑based) further compresses storage.

HyperLogLog (Fuzzy Counting)

HyperLogLog provides approximate unique counts with ~0.81% error while using only ~12KB, ideal for UV statistics.

pfadd sign_uv_${day} user_id
pfcount sign_uv_${day}

Bloom Filter

A Bloom filter offers a space‑efficient probabilistic set to filter out non‑existent users, reducing cache‑penetration queries.

def get_user_state(user_id):
    if not bloomfilter.is_user_exists(user_id):
        return {}
    return get_user_state0(user_id)

def save_user_state(user_id, state):
    bloomfilter.set_user_exists(user_id)
    save_user_state0(user_id, state)

Overall, the article demonstrates how Redis can replace many specialized components—locks, queues, rate limiters, discovery services, and probabilistic data structures—while keeping operations simple and performant.

HyperLogLogCachingBitmapbloom filterdelayed queuedistributed-lockrate-limitingservice-discovery
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.