Of course! Here is a comprehensive guide to using Redis with a Java client, covering the most popular client, Lettuce, with detailed examples, best practices, and comparisons.

Choosing a Java Client
There are two main, modern Java clients for Redis:
- Lettuce: The recommended and most popular choice for new projects. It is a fully non-blocking, asynchronous, and reactive client built on Netty. It supports synchronous, asynchronous, and reactive programming models.
- Jedis: The original and most well-known Java client. It is a blocking, synchronous client. While still widely used and functional, it's considered legacy for new development compared to Lettuce.
Recommendation: Use Lettuce for any new application. Its non-blocking nature provides better performance and scalability, especially in modern, high-concurrency environments like Spring Boot.
Getting Started with Lettuce
This section will walk you through setting up a simple Java project using Lettuce.
Step 1: Add Dependencies
You'll need the Lettuce core dependency and a connection pool. The connection pool is crucial for performance in most applications.

If you're using Maven, add this to your pom.xml:
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.3.2.RELEASE</version> <!-- Use the latest version -->
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.12.0</version> <!-- Use the latest version -->
</dependency>
If you're using Gradle, add this to your build.gradle:
implementation 'io.lettuce:lettuce-core:6.3.2.RELEASE' // Use the latest version implementation 'org.apache.commons:commons-pool2:2.12.0' // Use the latest version
Step 2: Basic Synchronous Example
This is the simplest way to interact with Redis. Lettuce handles the connection for you automatically.
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 LettuceSyncExample {
public static void main(String[] args) {
// 1. Create a Redis URI
// For a local Redis instance, RedisURI.create("redis://localhost")
RedisURI redisURI = RedisURI.create("redis://localhost:6379");
// 2. Create a Redis client
RedisClient redisClient = RedisClient.create(redisURI);
try {
// 3. Create a connection (this is a blocking, synchronous connection)
StatefulRedisConnection<String, String> connection = redisClient.connect();
// 4. Get a synchronous interface for commands
RedisCommands<String, String> syncCommands = connection.sync();
// 5. Execute commands
System.out.println("Setting a key: 'name' to 'Alice'");
syncCommands.set("name", "Alice");
System.out.println("Getting the value for key 'name': " + syncCommands.get("name"));
System.out.println("Setting a hash field: 'user:1001:email' to 'alice@example.com'");
syncCommands.hset("user:1001", "email", "alice@example.com");
syncCommands.hset("user:1001", "name", "Alice Smith");
System.out.println("Getting all fields for hash 'user:1001': " + syncCommands.hgetall("user:1001"));
// 6. Close the connection
connection.close();
} finally {
// 7. Shutdown the client
redisClient.shutdown();
}
}
}
Step 3: Using a Connection Pool (Recommended)
Creating a new connection for every operation is inefficient. A connection pool reuses connections, significantly improving performance.

import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.support.ConnectionPool;
import io.lettuce.core.support.ConnectionPoolSupport;
import java.util.concurrent.TimeUnit;
public class LettucePoolExample {
public static void main(String[] args) {
// 1. Create a Redis URI
RedisURI redisURI = RedisURI.create("redis://localhost:6379");
// 2. Create a Redis client
RedisClient redisClient = RedisClient.create(redisURI);
// 3. Create a connection pool
// The pool will manage up to 20 connections
ConnectionPool<StatefulRedisConnection<String, String>> pool = ConnectionPoolSupport.createGenericObjectPool(
() -> redisClient.connect(),
20 // max total connections
);
try {
// 4. Borrow a connection from the pool
StatefulRedisConnection<String, String> connection = pool.borrowObject();
try {
// 5. Use the connection
RedisCommands<String, String> syncCommands = connection.sync();
syncCommands.set("pool_key", "Hello from a pooled connection!");
System.out.println("Value from pool: " + syncCommands.get("pool_key"));
} finally {
// 6. Return the connection to the pool
pool.returnObject(connection);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 7. Shutdown the pool and the client
pool.close();
redisClient.shutdown();
}
}
}
Advanced Features of Lettuce
Asynchronous Programming
Lettuce is built on Netty, making asynchronous operations first-class citizens. This is great for high-throughput applications.
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.api.StatefulRedisConnection;
import java.util.concurrent.CompletableFuture;
public class LettuceAsyncExample {
public static void main(String[] args) {
RedisURI redisURI = RedisURI.create("redis://localhost:6379");
RedisClient redisClient = RedisClient.create(redisURI);
try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
// Get an asynchronous commands interface
RedisAsyncCommands<String, String> asyncCommands = connection.async();
// Commands now return CompletableFuture
CompletableFuture<String> futureSet = asyncCommands.set("async_key", "async_value");
CompletableFuture<String> futureGet = asyncCommands.get("async_key");
// You can chain operations or use callbacks
futureGet.thenAccept(value -> {
System.out.println("Async Get Result: " + value);
}).exceptionally(ex -> {
System.err.println("An error occurred: " + ex.getMessage());
return null;
});
// Wait for the asynchronous operations to complete
// In a real application, you'd use a more sophisticated event loop
futureSet.join(); // Blocks until this future is done
futureGet.join(); // Blocks until this future is done
System.out.println("Async operations completed.");
} finally {
redisClient.shutdown();
}
}
}
Reactive Programming (Project Reactor)
For modern, reactive stack applications (like Spring WebFlux), Lettuce provides a reactive API that integrates seamlessly with Project Reactor.
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.reactive.RedisReactiveCommands;
import reactor.core.publisher.Mono;
public class LettuceReactiveExample {
public static void main(String[] args) {
RedisURI redisURI = RedisURI.create("redis://localhost:6379");
RedisClient redisClient = RedisClient.create(redisURI);
try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
// Get a reactive commands interface
RedisReactiveCommands<String, String> reactiveCommands = connection.reactive();
// Commands return Mono or Flux
Mono<String> setMono = reactiveCommands.set("reactive_key", "reactive_value");
Mono<String> getMono = reactiveCommands.get("reactive_key");
// Chain operations using reactive operators
getMono
.doOnSuccess(value -> System.out.println("Reactive Get Result: " + value))
.doOnError(error -> System.err.println("Error: " + error.getMessage()))
.block(); // Block to see the result in this simple example
System.out.println("Reactive operation completed.");
} finally {
redisClient.shutdown();
}
}
}
Spring Boot Integration
Spring Boot makes using Redis incredibly easy with autoconfiguration. It defaults to using Lettuce with a connection pool.
Step 1: Add the Spring Boot Starter Data Redis
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Step 2: Configure Redis in application.properties
# application.properties or application.yml spring.redis.host=localhost spring.redis.port=6379 # For production, use a strong password # spring.redis.password=yourpassword
That's it! Spring Boot automatically configures a RedisTemplate and a StringRedisTemplate for you.
Step 3: Use @Autowired to Inject and Use RedisTemplate
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.Map;
@SpringBootApplication
public class RedisApplication implements CommandLineRunner {
// For String keys and values
@Autowired
private StringRedisTemplate stringRedisTemplate;
// For generic objects (requires serialization)
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
// --- Using StringRedisTemplate ---
System.out.println("--- Using StringRedisTemplate ---");
stringRedisTemplate.opsForValue().set("spring:name", "Bob");
String name = stringRedisTemplate.opsForValue().get("spring:name");
System.out.println("Name from Redis: " + name);
// --- Using RedisTemplate for Objects ---
System.out.println("\n--- Using RedisTemplate for Objects ---");
User user = new User(2L, "Charlie", "charlie@example.com");
redisTemplate.opsForValue().set("user:2", user);
User retrievedUser = (User) redisTemplate.opsForValue().get("user:2");
System.out.println("Retrieved User: " + retrievedUser);
// --- Using Hash Operations ---
System.out.println("\n--- Using Hash Operations ---");
stringRedisTemplate.opsForHash().put("user:3:details", "name", "David");
stringRedisTemplate.opsForHash().put("user:3:details", "email", "david@example.com");
Map<Object, Object> userDetails = stringRedisTemplate.opsForHash().entries("user:3:details");
System.out.println("User Details Hash: " + userDetails);
}
// A simple POJO to be stored in Redis
static class User {
private Long id;
private String name;
private String email;
// Constructors, Getters, and Setters are required for serialization
public User() {}
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', email='" + email + "'}";
}
// Getters and Setters...
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
}
Jedis (Legacy) Quick Comparison
If you encounter a project using Jedis, here's how it compares.
Key Differences
| Feature | Lettuce | Jedis |
|---|---|---|
| Model | Non-blocking, Asynchronous/Reactive | Blocking, Synchronous |
| I/O | Netty | Java Socket |
| Connection Handling | Can manage its own connections or use a pool. Requires a pool. | Requires a connection pool (JedisPool) for performance. |
| Threading | One connection per thread is not needed. A single thread can handle many connections. | Each connection is thread-unsafe. You must get a connection from the pool for each operation. |
| Recommendation | Recommended for new projects. | Legacy, but still functional. |
Jedis Code Example (for comparison)
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisExample {
public static void main(String[] args) {
// 1. Configure and create a connection pool (mandatory for performance)
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(20);
JedisPool jedisPool = new JedisPool(poolConfig, "localhost", 6379);
// 2. Borrow a resource from the pool
try (Jedis jedis = jedisPool.getResource()) {
// 3. Use the resource
jedis.set("jedis_key", "Hello from Jedis!");
String value = jedis.get("jedis_key");
System.out.println("Value from Jedis: " + value);
} finally {
// 4. Close the pool when the application shuts down
jedisPool.close();
}
}
}
Summary
- For new Java projects, always choose Lettuce.
- Lettuce supports synchronous, asynchronous, and reactive programming models.
- Always use a connection pool (Lettuce's
GenericObjectPoolor Spring Boot's auto-configured one) for production applications to manage connections efficiently. - Spring Boot makes integration trivial with
spring-boot-starter-data-redis, which defaults to Lettuce. - Jedis is the legacy, synchronous client. It works but is not suitable for modern, high-performance, non-blocking architectures.
