← Back to the section

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

SituationRedis StreamsKafka
Redis is already in the stacka good fitoverkill
You need partitioning across hundreds of topicsnot optimala good fit
Volume is millions of messages/seclimited by RAMa good fit
Replay over several days/weekslimited by policya good fit
Operational simplicity matters more than scalea good fitharder

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 lockSET 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.
  • 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.