Use Cases & Patterns
|
This content was generated with the assistance of AI and should be verified against the official Redis documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Examples on this page target Redis Open Source 8.10 (8.10.2) with the Jedis 8 and Lettuce 7.7 client libraries, and reuse the Bookshop scenario from the rest of this section — a bookshop’s cart, catalog, and storefront — rather than defining a new domain. Most patterns below are a handful of commands used in a specific shape; where a fuller treatment of the underlying data structure or command family already exists elsewhere in this section or in the application-layer pages, this page links to it instead of repeating it.
Caching: cache-aside, read-through, write-behind
Cache-aside (the default shape) has the application check the cache first, fall through to the database on a miss, then populate the cache before returning:
127.0.0.1:6379> GET bookshop:cache:book:9780000000001
(nil)
127.0.0.1:6379> SET bookshop:cache:book:9780000000001 "{\"title\":\"The Redis Way\",\"price\":19.99}" EX 3600
OK
A flat TTL makes every cached key expire in lockstep after a bulk warm-up, so many requests miss at once (a cache stampede). Jittering the TTL spreads expirations out, and a short-lived per-key lock lets only one caller recompute a value while the rest wait or serve the stale copy a little longer:
package com.example.bookshop;
import redis.clients.jedis.RedisClient;
import redis.clients.jedis.params.SetParams;
import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;
public class BookCacheService {
private final RedisClient redis;
public BookCacheService(RedisClient redis) {
this.redis = redis;
}
public String getBook(String isbn, java.util.function.Supplier<String> loadFromDb) {
String key = "bookshop:cache:book:" + isbn;
String cached = redis.get(key);
if (cached != null) {
return cached;
}
String lockKey = "bookshop:lock:book:" + isbn;
if (redis.set(lockKey, "1", SetParams.setParams().nx().px(5_000)) == null) {
return redis.get(key); // another caller is recomputing; retry/return stale
}
try {
String value = loadFromDb.get();
int jitterSeconds = ThreadLocalRandom.current().nextInt(60);
redis.set(key, value, SetParams.setParams().ex(Duration.ofHours(1).toSeconds() + jitterSeconds));
return value;
} finally {
redis.del(lockKey);
}
}
}
Read-through pushes this same miss-then-populate logic behind the cache client or a caching abstraction
(Spring’s @Cacheable, an ORM’s second-level cache) so application code never sees the miss path directly — see Second-Level Cache for that abstraction on the Java/Hibernate side.
Write-behind instead writes to the cache synchronously and queues the database write to happen asynchronously
(commonly via a stream), trading a small durability window for
lower write latency; it needs a retry/dead-letter path for the deferred write, unlike the simpler write-through
shape (write cache and database together, synchronously).
Prefetching populates hot keys before they’re requested — a scheduled job re-warms the top-selling titles' cache entries a few minutes before their TTL would otherwise lapse, avoiding the miss entirely for predictable hot data:
127.0.0.1:6379> EXPIRETIME bookshop:cache:book:9780000000001
(integer) 1732550400
This derives from Caching with Redis.
Session store
A session maps naturally to a Redis hash, one field per attribute, keyed per user or per session ID — bookshop:cart:<userId> for the Bookshop’s shopping cart is the same shape:
127.0.0.1:6379> HSET bookshop:cart:42 9780000000001 2 9780000000002 1
(integer) 2
127.0.0.1:6379> HEXPIRE bookshop:cart:42 1800 FIELDS 1 9780000000001
1) (integer) 1
127.0.0.1:6379> EXPIRE bookshop:cart:42 7200
(integer) 1
HEXPIRE key seconds FIELDS numfields field [field …] expires an individual hash field independently of the
key’s own TTL — useful when a session hash mixes long-lived attributes (user ID, cart total) with short-lived
ones (a one-time promo code applied to the cart) that should drop out on their own schedule. This is separate
from the framework-level session store already covered on this site: Spring Session’s Redis-backed
HttpSession implementation will be covered on the future Spring Boot caching page, and
Logout and Session Management already covers the OAuth/OIDC session-store material
(the client application’s own session versus the authorization server’s) that this page doesn’t repeat.
This derives from Session management with Redis and HEXPIRE.
Rate limiting
Fixed window: an atomic increment-with-expiry counter per window. INCREX key BYINT increment UBOUND cap EX
seconds ENX increments and enforces the cap in one round trip — ENX sets the TTL only when the key is first
created in a window, so later increments don’t push the window’s start out, and UBOUND makes the reply’s
actual-increment element 0 once the cap is reached, so the caller can check it directly instead of comparing
the returned value to the limit:
127.0.0.1:6379> INCREX bookshop:ratelimit:42 BYINT 1 UBOUND 100 EX 60 ENX
1) (integer) 1
2) (integer) 1
Fixed windows allow up to double the limit across a window boundary (a burst just before and just after the reset). Sliding window avoids that by keeping one sorted-set member per request, scored by its timestamp, trimming everything older than the window on each check:
127.0.0.1:6379> ZREMRANGEBYSCORE bookshop:ratelimit:42 -inf (1732550340000
(integer) 3
127.0.0.1:6379> ZADD bookshop:ratelimit:42 1732550400000 "req-9f1c"
(integer) 1
127.0.0.1:6379> ZCARD bookshop:ratelimit:42
(integer) 47
127.0.0.1:6379> EXPIRE bookshop:ratelimit:42 60
(integer) 1
Token bucket allows short bursts up to the bucket size while enforcing a steady long-run refill rate; because it needs to read the bucket’s current level, compute the elapsed-time refill, and write the new level all atomically, it’s naturally a Lua script rather than a single command:
-- KEYS[1] = bookshop:ratelimit:42, ARGV = capacity, refill_per_sec, now_ms, cost
local capacity, refill_rate, now, cost = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3]), tonumber(ARGV[4])
local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(bucket[1]) or capacity
local ts = tonumber(bucket[2]) or now
tokens = math.min(capacity, tokens + (now - ts) / 1000 * refill_rate)
if tokens < cost then
return 0
end
redis.call('HSET', KEYS[1], 'tokens', tokens - cost, 'ts', now)
redis.call('EXPIRE', KEYS[1], 60)
return 1
| Algorithm | Accuracy | Burst handling | Implementation cost |
|---|---|---|---|
Fixed window |
Allows up to 2x limit at window boundaries |
Poor — boundary burst |
One command ( |
Sliding window (sorted set) |
Exact, no boundary artifact |
None beyond the configured limit |
A few commands, one member per request (higher memory) |
Token bucket (Lua) |
Exact against the configured rate |
Allows a burst up to bucket capacity, then throttles to the refill rate |
One Lua script, small fixed state per key |
This derives from Redis rate limiting techniques,
INCREX, and
EVAL/EVALSHA for the Lua script.
On the JVM, the library alternative to a hand-rolled Lua token bucket is Bucket4j, which keeps the bucket in Redis
through a ProxyManager and adds bandwidths, per-key configuration and Spring Boot integration; see
Bucket4j: Token Bucket and Bandwidths and
Bucket4j: Distributed Rate Limiting.
Semantic cache and agent memory
A semantic cache stores an LLM prompt’s response keyed by the embedding of the prompt rather than its exact
text, so a paraphrased question ("what’s the return policy for a damaged book?" vs. "can I return a book that
arrived damaged?") can still hit the cache; agent memory uses the same shape to let an agent retrieve past
conversation turns or facts relevant to the current context. Both are a KNN/range vector query over an index
of past prompts or memories rather than a distinct feature — see
Vector Search: Query Engine & Vector Sets for FT.CREATE with a VECTOR field, FT.SEARCH … KNN, and the
distance-metric tradeoffs that apply equally here; this page adds nothing to that beyond the use case itself.
Reliable job queues
A Redis list makes a simple FIFO job queue: LPUSH enqueues, and BLMOVE source destination LEFT RIGHT
timeout atomically pops the oldest job and pushes it onto a per-worker "in-progress" list in one step, so a
worker that crashes mid-job doesn’t lose it — a reaper can periodically move stale entries back onto the main
queue:
127.0.0.1:6379> LPUSH bookshop:jobs:emails "{\"orderId\":1042}"
(integer) 1
127.0.0.1:6379> BLMOVE bookshop:jobs:emails bookshop:jobs:emails:processing LEFT RIGHT 5
"{\"orderId\":1042}"
For anything needing multiple independent consumers, replay, or per-message acknowledgment, a stream with
consumer groups is the better fit — bookshop:orders with a fulfilment group and worker-1/worker-2
consumers is exactly that shape, covered in full (including XREADGROUP, XACK, and claiming stuck messages
with XAUTOCLAIM) in Streams, Pub/Sub & Keyspace Notifications; this page doesn’t repeat it.
This derives from Redis as a message queue and BLMOVE.
Distributed locks
The base primitive is a SET key token NX PX ttl — NX makes acquisition atomic (only the first caller
succeeds), PX bounds how long a crashed holder can block others. Releasing must check that the caller still
holds the lock (its own random token) before deleting it, so one caller can’t release a lock a different
caller has since acquired after the first one’s TTL expired:
127.0.0.1:6379> SET bookshop:lock:inventory:9780000000001 "tok-a1b2" NX PX 30000
OK
127.0.0.1:6379> DELEX bookshop:lock:inventory:9780000000001 IFEQ "tok-a1b2"
(integer) 1
DELEX key IFEQ value performs that check-then-delete atomically as a single command; a Lua script comparing
GET against the token before DEL is the older, equivalent way to do it on a server without DELEX.
Whichever release mechanism is used, a lock alone doesn’t stop a caller that’s paused past its TTL (a long GC
pause, a slow network) from acting after another caller has already acquired the lock and started — a
fencing token, a number that increments on every successful acquisition and gets checked by whatever
downstream resource the lock protects, closes that gap by letting the resource reject a stale caller’s write
even if the lock itself has moved on.
Redlock extends the same SET NX PX primitive across N independent Redis masters, requiring a majority quorum
before considering the lock held, to survive a single master’s failure without losing the lock’s mutual-exclusion
guarantee — it needs a genuinely independent deployment (not just replicas of one master) to provide that
guarantee, and its safety properties under clock drift and long pauses are debated, so it is not a drop-in
replacement for a single-instance lock unless that multi-master independence is actually in place. ShedLock’s
Redis provider (RedisLockProvider, covered in
Scheduling and ShedLock) uses the single-instance SET … NX PX shape, which is
sufficient for its purpose of skipping a duplicate @Scheduled run rather than protecting a critical section
against a Byzantine failure.
This derives from Distributed locks with Redis and DELEX.
Leaderboards, dedup, unique visitors, geo, recommendations, feature store, fraud, and gateways
The remaining patterns are each a short, idiomatic use of one existing Bookshop data structure:
-
Leaderboards — a sorted set ranks by score with no separate sort step:
ZADD bookshop:bestsellers 1284 "9780000000001"records a book’s unit-sold count,ZREVRANGE bookshop:bestsellers 0 9 WITHSCORESreads the top 10. -
Dedup — a plain
SET(SISMEMBER) works for an exact, bounded set; a Bloom filter trades a small false- positive rate for constant, tiny memory at scale:BF.ADD bookshop:seen-isbns 9780000000001/BF.EXISTS bookshop:seen-isbns 9780000000001. A Cuckoo filter (CF.ADD/CF.EXISTS) adds the ability to remove an entry later, which a Bloom filter cannot do. -
Unique visitors — a HyperLogLog counts approximate distinct values in near-constant space:
PFADD bookshop:visitors:2026-09-25 "user:42"thenPFCOUNT bookshop:visitors:2026-09-25. -
Geo lookup —
GEOADD bookshop:stores -3.7038 40.4168 "madrid-centro"indexes a store’s coordinates,GEOSEARCH bookshop:stores FROMLONLAT -3.70 40.42 BYRADIUS 10 km ASCfinds the nearest ones. -
Recommendation by scores — a sorted set of a user’s recently viewed titles doubles as a lightweight "more like this" signal:
LPUSH bookshop:recent:42 9780000000001capped withLTRIM bookshop:recent:42 0 49feeds a downstream similarity step (see the vector-search page above for embedding-based recommendations). -
Feature store — a hash per entity, one field per feature, with a TTL matching how fresh the feature needs to be:
HSET bookshop:features:user:42 avg_order_value 34.20 churn_score 0.12thenEXPIRE bookshop:features:user:42 3600. -
Fraud detection — combines several of the above: a sliding-window counter on checkout attempts per card, a Bloom filter of previously flagged device fingerprints, and a feature-store hash of the user’s recent behavior, all read in one round trip via a pipeline (see Pipelining, Transactions & Scripting) so the scoring decision stays within a tight request-time budget.
-
API gateway — the same rate-limiting keys above, checked before proxying a request, plus a short-TTL cache-aside entry per upstream response for idempotent
GETroutes, keep both abuse and upstream load down without a dedicated gateway product.
Two existing pages already cover the caching half of this list on their own stacks and aren’t repeated here:
Second-Level Cache (Hibernate’s L2 cache, including a Redis-backed region
factory) and Performance and Caching (ASP.NET Core’s IDistributedCache, which a
Redis-backed implementation plugs into the same way).
This derives from Leaderboards with Redis, BF.ADD, CF.ADD, PFADD, and GEOADD.
Related pages
-
Vector Search: Query Engine & Vector Sets — the vector index and
KNNqueries behind semantic cache, agent memory, and embedding-based recommendations. -
Streams, Pub/Sub & Keyspace Notifications — consumer groups,
XREADGROUP, and message acknowledgment for the reliable-queue pattern above. -
Pipelining, Transactions & Scripting — batching multiple commands into one round trip, used by the fraud-detection pattern above.
-
Scheduling and ShedLock — ShedLock’s Redis lock provider for skipping duplicate
@Scheduledruns. -
Quartz with Redis — Quartz job stores on Redis, built on the same locking primitives.
-
Second-Level Cache — Hibernate’s own cache-region abstraction.
-
Logout and Session Management — the client-vs-IdP session distinction for federated login.
-
Agent Memory & Semantic Cache — the vendor-neutral pattern behind this page’s semantic-cache use case.
-
Performance and Caching — ASP.NET Core’s
IDistributedCacheand output caching.
References
-
Caching with Redis — cache-aside, read-through, write-behind/write- through.
-
Redis rate limiting techniques — fixed window, sliding window, and token bucket compared.
-
EVAL.
-
Distributed locks with Redis —
SET NX PX, fencing tokens, and Redlock. -
DELEX — conditional delete, including
IFEQ.