Redis most often enters a project as a cache. But those same data structures and atomic operations solve several other classic backend problems — without pulling in separate services.
Why Go Beyond Caching at All
When an application runs in multiple instances, the familiar tools start to fail. A synchronized block protects only a single JVM process. AtomicLong lives only in the memory of one Pod. Guava's EventBus knows nothing about the neighbouring servers.
Redis works as a single point of coordination: all application instances see the same state, and operations on it are atomic. This is the very foundation for the patterns we will look at below.
Distributed Lock
The Problem
Several application instances start the same job or process the same event at the same time. You need to guarantee that only one instance runs the critical section.
SETNX + TTL: The Simple Option
SETNX (SET if Not eXists) is an atomic command: the key is created only if it does not already exist.
# Try to acquire the lock for 30 seconds
SET lock:invoice:42 owner-uuid-1234 NX EX 30
# OK — the lock is acquired
# (nil) — someone else already holds the lock
NX means "only if it does not exist", EX 30 is the TTL in seconds. The TTL is mandatory: if the process crashes, the lock is released automatically.
You must release only your own lock — so we write a unique owner identifier into the value and check it before deleting:
# Check the owner and delete — atomically via Lua
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:invoice:42 owner-uuid-1234
Fencing Token
A simple SETNX lock has a weakness: if the process hangs, the TTL expires, another instance acquires the lock — and then the first one "wakes up" and assumes it still holds it. For critical operations you add a fencing token — a monotonically increasing number passed with every request to the protected resource. The resource rejects requests carrying an old token.
In Redis such a counter can be stored with the INCR command.
Redlock and Its Criticism
Redlock is a distributed lock algorithm on top of several independent Redis nodes. It protects against the situation where a single Redis node becomes unavailable.
A short note on the debate: it is known that Redlock does not provide strict guarantees under process stalls (GC pauses, network partitions). The Redis authors argue that Redlock is sufficient for most real-world cases. The takeaway: if you need absolute correctness (financial transactions), use a fencing token or a specialized service (ZooKeeper, etcd). For tasks like "don't run a job twice", Redlock is a good fit.
A ready-made implementation for Java is the Redisson library (RLock).
Rate Limiting
Counter with TTL
The simplest rate limiter: count requests within a period and block once the limit is exceeded.
# Every request from user user:42
INCR rate:user:42:2024060312 # key = user + hour
EXPIRE rate:user:42:2024060312 3600
If the counter value has exceeded the limit — return 429. The TTL guarantees the key is deleted after an hour.
The downside: fixed window — at the boundary of two windows you can "double" the limit. In the last second of a window and the first second of the next, twice as many requests can go through.
Sliding Window
More accurate, but requires more memory. For each request we store a timestamp in a Sorted Set (ZADD), and before checking we remove the stale entries (ZREMRANGEBYSCORE):
# ts = current timestamp in milliseconds
ZADD rate:user:42 1717425600000 "req-uuid"
ZREMRANGEBYSCORE rate:user:42 0 1717425600000-60000 # remove older than 60 sec
ZCARD rate:user:42 # how many requests are in the window
EXPIRE rate:user:42 60
If ZCARD > limit — reject. The whole pipeline runs atomically via MULTI/EXEC or a Lua script.
In Spring Boot the built-in support comes through RedisTemplate or the Bucket4j library with a Redis backend.
Pub/Sub: Publish and Subscribe
Pub/Sub is a channel for sending messages to all subscribers in real time.
# Subscriber
SUBSCRIBE channel:notifications
# Publisher (from another client)
PUBLISH channel:notifications "user:42 logged in"
In Spring Data Redis — RedisMessageListenerContainer with a MessageListener.
When Pub/Sub Fits and When It Doesn't
Pub/Sub is a good fit for tasks where losing a single message is not critical: real-time UI updates, cache invalidation across several servers, "best-effort" notification broadcasts.
Pub/Sub is not a fit if you need to:
- guarantee delivery (a subscriber that was not listening at the moment of publication will not receive the message);
- replay the history of events;
- process a message exactly once.
For these cases — Redis Streams.
Redis Streams: A Reliable Queue
Streams appeared in Redis 5.0 as an append-only log — analogous to Kafka, but without a separate cluster.
# Publish an event
XADD orders * order_id 1001 status created
# Read with a consumer group
XREADGROUP GROUP order-processor consumer-1 COUNT 10 BLOCK 2000 STREAMS orders >
# Acknowledge processing
XACK orders order-processor 1717425600000-0
> means "give me new messages that no one in the group has taken yet". After processing we call XACK — without it the message stays in the "being processed" state and remains visible through XPENDING.
Streams give you:
- message durability (a message is not lost when a consumer disconnects);
- consumer groups with load balancing;
- reprocessing of undelivered messages.
In Java/Spring — StreamMessageListenerContainer from Spring Data Redis.
Streams vs Kafka: When to Choose Which
| Situation | Redis Streams | Kafka |
|---|---|---|
| Redis is already in the stack | a good fit | overkill |
| You need partitioning across hundreds of topics | not optimal | a good fit |
| Volume is millions of messages/sec | limited by RAM | a good fit |
| Replay over several days/weeks | limited by policy | a good fit |
| Operational simplicity matters more than scale | a good fit | harder |
The short formula: Redis Streams is a reasonable choice when Redis is already present and the volume and retention requirements fit within its capabilities.
In Short
- Distributed lock —
SET key value NX EX ttl; the owner is identified by a unique value; release via a Lua script. - Fencing token — a monotonic counter (
INCR) as protection against a "woken-up" stale lock. - Redlock fits most coordination tasks, but not operations that require strict correctness under GC pauses.
- Rate limiting: a simple counter with a TTL (fixed window) or a Sorted Set (sliding window) for an accurate sliding window.
- Pub/Sub — "best-effort" delivery: no delivery guarantee, no history. Great for cache invalidation and real-time notifications.
- Streams — a reliable queue with consumer groups and acknowledgement; choose it when Redis is already present and Kafka is overkill.
What to Read Next
- Redis data structures — List, Sorted Set, Hash, Bitmap — the foundation for all the patterns above.
- Caching patterns — cache-aside, write-through, TTL and entry expiration.
- Operating Redis — replication, Redis Sentinel, Cluster, RDB and AOF.