Redis #
Databases are almost always the first bottleneck when traffic increases. Queries that used to be fast start feeling slow when executed hundreds of times per second, and schemas that seemed efficient start struggling with ever-growing data volumes. Redis steps in as the layer between the application and the database — an in-memory data store capable of serving millions of operations per second with sub-millisecond latency. But Redis isn’t just a simple cache. It’s a data structure server supporting String, Hash, List, Set, Sorted Set, and much more — each with rich operation semantics that can be used to solve problems far beyond simple caching, from rate limiting and session management to distributed locks, real-time leaderboards, and lightweight message queues.
Choosing a Client Library #
In the Java ecosystem, there are two most widely used Redis client libraries: Jedis and Lettuce. Both are mature and production-tested, but have different characteristics.
| Jedis | Lettuce | |
|---|---|---|
| I/O model | Synchronous (blocking) | Asynchronous & Reactive |
| Thread safety | No — needs a connection pool | Yes — one connection can be used by many threads |
| Spring Boot default | No (used to be, now Lettuce) | Yes (since Spring Boot 2) |
| Ease of use | Very easy, intuitive API | Slightly more complex |
| High throughput | Needs a large pool | Efficient with few connections |
| Redis Cluster | Supported | Supported, more mature |
For most projects, Jedis with a connection pool is the easiest place to start. Lettuce fits better if you need reactive programming or Spring WebFlux integration.
Setting Up Dependencies #
Jedis #
<!-- Maven -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.1.3</version>
</dependency>
// Gradle
implementation 'redis.clients:jedis:5.1.3'
Lettuce #
<!-- Maven -->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.3.2.RELEASE</version>
</dependency>
// Gradle
implementation 'io.lettuce:lettuce-core:6.3.2.RELEASE'
To run Redis locally:
# Docker — the fastest way
docker run -d \
--name redis \
-p 6379:6379 \
redis:7.2-alpine
# With a password
docker run -d \
--name redis \
-p 6379:6379 \
redis:7.2-alpine \
redis-server --requirepass "secret123"
# Access the Redis CLI
docker exec -it redis redis-cli
Connections and Connection Pools #
Jedis with JedisPool #
Jedis isn’t thread-safe, so you need a connection pool. Each thread takes a connection from the pool, uses it, then returns it to the pool.
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class RedisConnectionFactory {
// ✗ ANTI-PATTERN: creating a new Jedis every time you need it
// Every new Jedis() opens a new TCP connection — very expensive
public static Jedis createInsecure() {
return new Jedis("localhost", 6379); // don't use this in production
}
// ✓ CORRECT: use a JedisPool — one pool, many reused connections
public static JedisPool createPool() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(20); // max 20 active connections
config.setMaxIdle(10); // max 10 idle connections
config.setMinIdle(2); // keep at least 2 connections
config.setTestOnBorrow(true); // validate connections before use
config.setTestOnReturn(true); // validate connections when returned
config.setBlockWhenExhausted(true); // wait if the pool is full (don't throw immediately)
config.setMaxWait(java.time.Duration.ofSeconds(5)); // wait max 5 seconds
// Without a password
return new JedisPool(config, "localhost", 6379);
// With a password
// return new JedisPool(config, "localhost", 6379, 2000, "secret123");
}
// The correct usage — try-with-resources returns it to the pool automatically
public static void usageExample(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
jedis.set("key", "value");
String value = jedis.get("key");
System.out.println("Value: " + value);
} // jedis is automatically returned to the pool here
}
}
Lettuce — A Single Connection for All Threads #
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
public class LettuceConnectionFactory {
public static RedisClient createClient() {
RedisURI redisUri = RedisURI.builder()
.withHost("localhost")
.withPort(6379)
// .withPassword("secret123".toCharArray())
.withDatabase(0)
.withTimeout(java.time.Duration.ofSeconds(5))
.build();
return RedisClient.create(redisUri);
}
public static void usageExample(RedisClient client) {
// One StatefulRedisConnection can be used from many threads
try (StatefulRedisConnection<String, String> connection = client.connect()) {
RedisCommands<String, String> commands = connection.sync();
commands.set("key", "value");
String value = commands.get("key");
System.out.println("Value: " + value);
}
// Close the client when the application shuts down
// client.shutdown();
}
}
The String Data Structure #
String is Redis’s most basic data type. Despite the name, it can store text, numbers, or binary data (up to 512 MB).
public class RedisStringDemo {
private final JedisPool pool;
public RedisStringDemo(JedisPool pool) {
this.pool = pool;
}
public void demo() {
try (Jedis jedis = pool.getResource()) {
// Basic SET and GET
jedis.set("name", "Budi");
String name = jedis.get("name");
System.out.println("name: " + name); // "Budi"
// SET with TTL (expire) — the key is automatically deleted after N seconds
jedis.setex("session:abc123", 3600, "{\"userId\":42,\"role\":\"admin\"}");
// or use SET with the EX option
jedis.set("token:xyz", "bearer-token", redis.clients.jedis.params.SetParams.setParams().ex(1800));
// Check the remaining TTL
long ttl = jedis.ttl("session:abc123");
System.out.println("Remaining TTL: " + ttl + " seconds");
// INCR / DECR — atomic increment for counters
jedis.set("counter:views", "0");
jedis.incr("counter:views"); // 1
jedis.incr("counter:views"); // 2
jedis.incrBy("counter:views", 10); // 12
System.out.println("Views: " + jedis.get("counter:views")); // "12"
// SETNX — set only if the key doesn't exist (for simple distributed locks)
boolean set = jedis.setnx("lock:resource", "1") == 1;
System.out.println("Lock acquired: " + set);
// GETSET — get the old value, set the new value atomically
String oldValue = jedis.getSet("name", "Andi");
System.out.println("Old value: " + oldValue + ", new value: " + jedis.get("name"));
// MGET / MSET — batch operations
jedis.mset("city", "Jakarta", "country", "Indonesia", "continent", "Asia");
java.util.List<String> results = jedis.mget("city", "country", "continent");
System.out.println("Batch: " + results); // [Jakarta, Indonesia, Asia]
// EXISTS and DELETE
boolean exists = jedis.exists("name");
jedis.del("name", "city"); // delete multiple keys at once
}
}
}
The Hash Data Structure #
Hashes store field-value pairs under a single key. They’re perfect for representing objects — more efficient than storing a JSON string because fields can be accessed or updated individually.
public class RedisHashDemo {
public void demo(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
// HSET — set one or more fields
jedis.hset("user:1001", "name", "Sari");
jedis.hset("user:1001", "email", "[email protected]");
jedis.hset("user:1001", "age", "28");
// Or set many fields at once
jedis.hset("user:1002", java.util.Map.of(
"name", "Budi",
"email", "[email protected]",
"age", "35",
"city", "Bandung"
));
// HGET — get a single field
String email = jedis.hget("user:1001", "email");
System.out.println("Email: " + email);
// HGETALL — get all fields and values
java.util.Map<String, String> user = jedis.hgetAll("user:1001");
System.out.println("User: " + user);
// HMGET — get several fields at once
java.util.List<String> fields = jedis.hmget("user:1002", "name", "city");
System.out.println("Name and city: " + fields);
// HINCRBY — increment a numeric field atomically
jedis.hincrBy("user:1001", "age", 1); // birthday!
// HEXISTS — check whether a field exists
boolean hasCity = jedis.hexists("user:1001", "city");
System.out.println("Has a city field: " + hasCity); // false
// HDEL — delete specific fields
jedis.hdel("user:1002", "city");
// HKEYS / HVALS / HLEN
java.util.Set<String> keys = jedis.hkeys("user:1002");
java.util.List<String> values = jedis.hvals("user:1002");
long fieldCount = jedis.hlen("user:1002");
System.out.println("Fields: " + keys + " | Count: " + fieldCount);
}
}
}
The List Data Structure #
Lists are linked lists supporting push and pop operations from both ends. Suitable for queues, stacks, activity feeds, and bounded logs.
public class RedisListDemo {
public void demo(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
// LPUSH / RPUSH — add elements to the left / right
jedis.rpush("queue:email", "email-1", "email-2", "email-3");
jedis.lpush("queue:email", "priority-email"); // goes to the front
// LRANGE — get elements in a range (0 = first, -1 = last)
java.util.List<String> all = jedis.lrange("queue:email", 0, -1);
System.out.println("Queue contents: " + all);
// [priority-email, email-1, email-2, email-3]
// LLEN — list length
long length = jedis.llen("queue:email");
System.out.println("Count: " + length);
// LPOP / RPOP — get and remove from the left / right
String first = jedis.lpop("queue:email"); // dequeue from the front
System.out.println("Processed: " + first); // priority-email
// BLPOP — blocking pop — waits until an element is available (ideal for workers)
// If the list is empty, blocks until an element arrives or the timeout (seconds)
java.util.List<String> result = jedis.blpop(5, "queue:email", "queue:sms");
if (result != null) {
System.out.println("Queue: " + result.get(0) + " | Message: " + result.get(1));
}
// LINSERT — insert an element before / after a specific element
jedis.linsert("queue:email", redis.clients.jedis.args.ListDirection.BEFORE,
"email-2", "email-1.5");
// LTRIM — trim the list, keep only elements in the range
// Useful for keeping a list from growing unbounded
jedis.rpush("log:activity", "login", "open-page", "logout");
jedis.ltrim("log:activity", 0, 99); // keep only the last 100
}
}
}
Pattern: A Simple Task Queue with Lists #
public class RedisTaskQueue {
private static final String QUEUE_KEY = "tasks:pending";
private static final String PROCESSING_KEY = "tasks:processing";
private final JedisPool pool;
public RedisTaskQueue(JedisPool pool) {
this.pool = pool;
}
// Producer: add a task to the queue
public void enqueue(String task) {
try (Jedis jedis = pool.getResource()) {
jedis.rpush(QUEUE_KEY, task);
}
}
// Consumer: take and process a task (blocking)
public void startWorker() {
System.out.println("Worker ready to process tasks...");
while (true) {
try (Jedis jedis = pool.getResource()) {
// BRPOPLPUSH — atomic: take from the queue, move to the processing list
// If a worker crashes, the task is still in the processing list
String task = jedis.brpoplpush(QUEUE_KEY, PROCESSING_KEY, 5);
if (task == null) continue; // timeout, try again
try {
System.out.println("Processing: " + task);
doWork(task);
// Remove from the processing list after finishing
jedis.lrem(PROCESSING_KEY, 1, task);
} catch (Exception e) {
System.err.println("Failed to process task: " + e.getMessage());
// The task stays in the processing list for manual recovery
}
}
}
}
private void doWork(String task) throws Exception {
Thread.sleep(100); // simulate work
}
}
The Set Data Structure #
Sets are unordered collections of unique strings. Suitable for tags, membership checks, and set operations (union, intersection, difference).
public class RedisSetDemo {
public void demo(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
// SADD — add one or more members
jedis.sadd("tag:article:1", "java", "redis", "backend", "tutorial");
jedis.sadd("tag:article:2", "java", "spring", "backend", "microservice");
// SMEMBERS — get all members
java.util.Set<String> tags = jedis.smembers("tag:article:1");
System.out.println("Tags: " + tags);
// SISMEMBER — membership check — O(1)
boolean hasJava = jedis.sismember("tag:article:1", "java");
System.out.println("Has the java tag: " + hasJava);
// SCARD — member count
long count = jedis.scard("tag:article:1");
System.out.println("Tag count: " + count);
// Set operations
// SINTER — intersection (tags in BOTH articles)
java.util.Set<String> intersection = jedis.sinter("tag:article:1", "tag:article:2");
System.out.println("Shared tags: " + intersection); // [java, backend]
// SUNION — union (all tags from all articles)
java.util.Set<String> union = jedis.sunion("tag:article:1", "tag:article:2");
System.out.println("All tags: " + union);
// SDIFF — difference (tags in article 1 that aren't in article 2)
java.util.Set<String> difference = jedis.sdiff("tag:article:1", "tag:article:2");
System.out.println("Unique tags in article 1: " + difference); // [redis, tutorial]
// SREM — remove members
jedis.srem("tag:article:1", "tutorial");
// SRANDMEMBER — get random members (for recommendations, sampling)
String random = jedis.srandmember("tag:article:1");
java.util.List<String> someRandom = jedis.srandmember("tag:article:1", 2);
}
}
}
The Sorted Set Data Structure #
Sorted Sets are like Sets, but each member has a score (a float). Members are ordered by score. This is Redis’s most versatile data structure — suitable for leaderboards, priority queues, and score-based range queries.
public class RedisSortedSetDemo {
public void demo(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
// ZADD — add members with scores
jedis.zadd("leaderboard:game1", 1500.0, "player:alice");
jedis.zadd("leaderboard:game1", 2300.0, "player:budi");
jedis.zadd("leaderboard:game1", 1800.0, "player:citra");
jedis.zadd("leaderboard:game1", 3100.0, "player:dani");
jedis.zadd("leaderboard:game1", 2100.0, "player:eka");
// ZRANGE — get members by rank (ascending, index 0 = lowest score)
java.util.List<String> lowest = jedis.zrange("leaderboard:game1", 0, -1);
System.out.println("Lowest to highest: " + lowest);
// ZREVRANGE — highest to lowest order (top players)
java.util.List<String> top3 = jedis.zrevrange("leaderboard:game1", 0, 2);
System.out.println("Top 3: " + top3); // [dani, budi, eka]
// ZRANGEBYSCORE — get members with scores in a certain range
java.util.List<String> middle = jedis.zrangeByScore(
"leaderboard:game1", 1800, 2300);
System.out.println("Score 1800-2300: " + middle);
// ZSCORE — get one member's score
Double score = jedis.zscore("leaderboard:game1", "player:alice");
System.out.println("Alice's score: " + score);
// ZRANK / ZREVRANK — position in the ranking (0-based)
Long rankAsc = jedis.zrank("leaderboard:game1", "player:budi");
Long rankDesc = jedis.zrevrank("leaderboard:game1", "player:budi");
System.out.println("Budi's rank (asc): " + rankAsc + " | (desc): " + rankDesc);
// ZINCRBY — increment a score atomically (for real-time score updates)
jedis.zincrBy("leaderboard:game1", 500.0, "player:alice");
// ZCARD — member count
long total = jedis.zcard("leaderboard:game1");
// ZCOUNT — count members within a score range
long count = jedis.zcount("leaderboard:game1", 2000, 3000);
System.out.println("Players with scores 2000-3000: " + count);
// ZREM — remove members
jedis.zrem("leaderboard:game1", "player:eka");
}
}
}
TTL and Eviction Policies #
Redis in production must be configured correctly to avoid running out of memory.
Setting TTLs #
public class RedisTTLDemo {
public void demo(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
// Set a TTL when creating the key
jedis.setex("session:user123", 1800, "session-data"); // 30 minutes
// Add a TTL to an existing key
jedis.set("cache:product:456", "{...}");
jedis.expire("cache:product:456", 300); // 5 minutes
// Remove the TTL — make the key permanent again
jedis.persist("cache:product:456");
// Check the TTL
long ttl = jedis.ttl("session:user123"); // in seconds, -1 = no TTL, -2 = no key
long pttl = jedis.pttl("session:user123"); // in milliseconds
// Check when a key expires (Unix timestamp in milliseconds)
jedis.expireAt("token:abc", System.currentTimeMillis() / 1000 + 3600); // expires 1 hour from now
}
}
}
Eviction Policies #
When Redis reaches its memory limit (maxmemory), it runs an eviction policy to automatically remove keys. Choose the right policy:
noeviction → reject new writes when memory is full (default, bad for caches)
allkeys-lru → evict the least recently used (LRU) keys from all keys
volatile-lru → evict the least recently used keys with a TTL
allkeys-lfu → evict the least frequently used (LFU) keys from all keys
volatile-lfu → evict the least frequently used keys with a TTL
allkeys-random → evict keys randomly from all keys
volatile-random → evict keys with a TTL randomly
volatile-ttl → evict the keys closest to expiring
For general caching use cases, allkeys-lru or allkeys-lfu are the best choices:
# In redis.conf
maxmemory 512mb
maxmemory-policy allkeys-lru
# Or via the CLI
redis-cli CONFIG SET maxmemory 512mb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
Caching Patterns #
Cache-Aside (Lazy Loading) #
The most common pattern — data is loaded into the cache only when needed:
public class CacheAsidePattern {
private final JedisPool pool;
private final DataRepository repository; // simulates database access
public CacheAsidePattern(JedisPool pool, DataRepository repository) {
this.pool = pool;
this.repository = repository;
}
public String getProduct(String productId) {
String cacheKey = "cache:product:" + productId;
try (Jedis jedis = pool.getResource()) {
// 1. Check the cache first
String cached = jedis.get(cacheKey);
if (cached != null) {
System.out.println("Cache HIT for product " + productId);
return cached;
}
// 2. Cache MISS — fetch from the database
System.out.println("Cache MISS for product " + productId);
String data = repository.findProductById(productId);
if (data != null) {
// 3. Store in the cache with a TTL
jedis.setex(cacheKey, 300, data); // cache for 5 minutes
}
return data;
}
}
// Invalidate the cache when data changes
public void updateProduct(String productId, String data) {
repository.updateProduct(productId, data);
try (Jedis jedis = pool.getResource()) {
// Delete the old cache — it will be reloaded on the next access
jedis.del("cache:product:" + productId);
}
}
interface DataRepository {
String findProductById(String id);
void updateProduct(String id, String data);
}
}
Write-Through #
Data is written to the cache and the database simultaneously:
public class WriteThroughPattern {
private final JedisPool pool;
private final DataRepository repository;
private static final int CACHE_TTL = 600; // 10 minutes
public WriteThroughPattern(JedisPool pool, DataRepository repository) {
this.pool = pool;
this.repository = repository;
}
public void saveProduct(String productId, String data) {
// Write to the database
repository.updateProduct(productId, data);
// Update the cache immediately — no stale data window
try (Jedis jedis = pool.getResource()) {
jedis.setex("cache:product:" + productId, CACHE_TTL, data);
}
}
public String getProduct(String productId) {
try (Jedis jedis = pool.getResource()) {
String cached = jedis.get("cache:product:" + productId);
if (cached != null) return cached;
return repository.findProductById(productId);
}
}
interface DataRepository {
String findProductById(String id);
void updateProduct(String id, String data);
}
}
Distributed Locks #
Redis is often used to implement distributed locks — ensuring only one process executes a critical section at a time in a distributed environment.
import java.util.UUID;
public class RedisDistributedLock {
private final JedisPool pool;
private static final String LOCK_PREFIX = "lock:";
private static final int DEFAULT_TIMEOUT_MS = 5000; // 5 seconds
public RedisDistributedLock(JedisPool pool) {
this.pool = pool;
}
// Try to acquire the lock
// Returns a lockToken on success, null if the lock is held by another process
public String tryAcquire(String resourceName, int ttlSeconds) {
String lockKey = LOCK_PREFIX + resourceName;
String lockToken = UUID.randomUUID().toString(); // unique token to identify the owner
try (Jedis jedis = pool.getResource()) {
// SET NX EX — set only if it doesn't exist, with a TTL
// This is atomic — no race condition between the check and the set
String result = jedis.set(
lockKey,
lockToken,
redis.clients.jedis.params.SetParams.setParams()
.nx() // only set if the key does NOT exist
.ex(ttlSeconds) // automatic TTL — the lock never hangs forever
);
if ("OK".equals(result)) {
System.out.println("Lock acquired: " + resourceName + " (token: " + lockToken + ")");
return lockToken;
}
System.out.println("Lock unavailable: " + resourceName);
return null;
}
}
// Release the lock — only if the token matches (you actually hold the lock)
public boolean release(String resourceName, String lockToken) {
String lockKey = LOCK_PREFIX + resourceName;
try (Jedis jedis = pool.getResource()) {
// ANTI-PATTERN: GET then DEL — there's a race condition between the two
// String current = jedis.get(lockKey);
// if (lockToken.equals(current)) jedis.del(lockKey); // ← not atomic!
// ✓ CORRECT: use a Lua script for an atomic GET + DEL operation
String luaScript =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
" return redis.call('DEL', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Object result = jedis.eval(luaScript,
java.util.List.of(lockKey),
java.util.List.of(lockToken)
);
boolean released = Long.valueOf(1).equals(result);
if (released) {
System.out.println("Lock released: " + resourceName);
} else {
System.out.println("Failed to release the lock — not the owner or already expired: " + resourceName);
}
return released;
}
}
// Execute a block of code with a lock
public <T> T withLock(String resourceName, int ttlSeconds,
java.util.concurrent.Callable<T> action) throws Exception {
String token = null;
long deadline = System.currentTimeMillis() + DEFAULT_TIMEOUT_MS;
// Retry until the lock is available or the timeout
while (System.currentTimeMillis() < deadline) {
token = tryAcquire(resourceName, ttlSeconds);
if (token != null) break;
Thread.sleep(50 + (long)(Math.random() * 50)); // jitter to avoid a thundering herd
}
if (token == null) {
throw new RuntimeException("Failed to acquire the lock for: " + resourceName);
}
try {
return action.call();
} finally {
release(resourceName, token);
}
}
}
sequenceDiagram
participant A as Service A
participant B as Service B
participant R as Redis
A->>R: SET lock:order NX EX 30 "token-A"
R-->>A: OK (lock acquired)
B->>R: SET lock:order NX EX 30 "token-B"
R-->>B: nil (lock already exists)
Note over A: execute critical logic
A->>R: EVAL lua — DEL if the token matches
R-->>A: 1 (successfully released)
B->>R: SET lock:order NX EX 30 "token-B"
R-->>B: OK (can now acquire the lock)Pipelines — Reducing Round Trips #
Every Redis command needs one round trip to the server. Pipelines send many commands at once and read all the responses at the end — very effective for batch operations.
public class RedisPipelineDemo {
public void demo(JedisPool pool) {
try (Jedis jedis = pool.getResource()) {
// ✗ ANTI-PATTERN: sending one by one — N round trips for N commands
for (int i = 0; i < 100; i++) {
jedis.set("key:" + i, "value:" + i);
}
// ✓ CORRECT: pipeline — 1 round trip for 100 commands
redis.clients.jedis.Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 100; i++) {
pipeline.set("key:" + i, "value:" + i);
pipeline.expire("key:" + i, 300);
}
// Send everything at once and wait for the responses
java.util.List<Object> responses = pipeline.syncAndReturnAll();
System.out.println("Pipeline done: " + responses.size() + " responses received");
}
}
// Pipeline for batch reads
public java.util.List<String> batchGet(JedisPool pool, java.util.List<String> keys) {
try (Jedis jedis = pool.getResource()) {
redis.clients.jedis.Pipeline pipeline = jedis.pipelined();
java.util.List<redis.clients.jedis.Response<String>> futures = new java.util.ArrayList<>();
for (String key : keys) {
futures.add(pipeline.get(key));
}
pipeline.sync();
java.util.List<String> results = new java.util.ArrayList<>();
for (redis.clients.jedis.Response<String> future : futures) {
results.add(future.get());
}
return results;
}
}
}
Pub/Sub in Redis #
Redis supports a simple publish/subscribe pattern. Unlike Kafka or RabbitMQ, Redis Pub/Sub doesn’t store messages — if there’s no subscriber when a message is sent, the message is lost. Suitable for real-time notifications, cache invalidation, and simple events.
import redis.clients.jedis.JedisPubSub;
public class RedisPubSubDemo {
// Subscriber — runs on a separate thread
public static class NotificationSubscriber extends JedisPubSub {
@Override
public void onMessage(String channel, String message) {
System.out.printf("[%s] Message received: %s%n", channel, message);
// Process the message
}
@Override
public void onSubscribe(String channel, int subscribedChannels) {
System.out.println("Subscribed to channel: " + channel);
}
@Override
public void onUnsubscribe(String channel, int subscribedChannels) {
System.out.println("Unsubscribed from channel: " + channel);
}
}
public static void startSubscriber(JedisPool pool) {
// Subscribing must run on a separate thread because the operation is blocking
Thread subscriberThread = new Thread(() -> {
try (Jedis jedis = pool.getResource()) {
NotificationSubscriber subscriber = new NotificationSubscriber();
// Subscribe to one or more channels
jedis.subscribe(subscriber, "notif:order", "notif:payment");
// or subscribe using a pattern
// jedis.psubscribe(subscriber, "notif:*");
}
});
subscriberThread.setDaemon(true);
subscriberThread.start();
}
public static void publish(JedisPool pool, String channel, String message) {
try (Jedis jedis = pool.getResource()) {
long subscriberCount = jedis.publish(channel, message);
System.out.printf("Message sent to %d subscribers on channel '%s'%n",
subscriberCount, channel);
}
}
}
When to Use Redis and When Not To #
USE REDIS WHEN:
✓ A cache layer to reduce database load (sessions, query results, pages)
✓ Rate limiting — atomic counters per IP/user with TTLs
✓ Distributed locks — coordination between instances/pods
✓ Leaderboards and rankings — Sorted Sets with ZINCRBY
✓ Session stores — Hashes per session with TTLs
✓ Simple task queues — Lists with BRPOPLPUSH
✓ Lightweight Pub/Sub — real-time notifications, cache invalidation
✓ Simple counting and analytics — INCR, HINCRBY
CONSIDER ALTERNATIVES WHEN:
✗ Data must never be lost → a relational database
✗ Very limited memory and large data → Memcached (more memory efficient)
✗ You need complex queries over cached data → store it in the DB, not Redis
✗ You need a reliable message queue with a DLQ → RabbitMQ, Kafka, or SQS
✗ Data larger than the available RAM → Redis isn't suitable as a primary store
Summary #
- Use a JedisPool, not a new connection per request — open the Jedis in try-with-resources so it’s automatically returned to the pool when done.
- Strings suit atomic counters (
INCR), session tokens, and simple caches. Hashes suit objects with many individually updated fields.- Lists support queues with
RPUSH/BLPOPand stacks withLPUSH/LPOP.BLPOPis the efficient way to run worker queues without busy loops.- Sets for O(1) membership checks and set operations (union, intersection, difference). Sorted Sets for leaderboards, priority queues, and score-based range queries.
- Always set TTLs on cache keys — use
SETEXorSET ... EX. Configuremaxmemory-policy allkeys-lrufor caches that manage memory automatically.- Cache-aside is the most common pattern — read from the cache, go to the database on a miss, then store in the cache. Always invalidate the cache when data changes.
- Distributed locks use
SET NX EX(atomic) and a Lua script for safe release — don’t use separate GET + DEL because of the race condition.- Pipelines drastically reduce round trips for batch operations — use them when you need to set or get many keys at once.
- Redis Pub/Sub suits lightweight real-time notifications, not as a replacement for a reliable message broker — messages are lost if there’s no subscriber.