Technical Documentation: redis/jedis
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
Jedis
What is Jedis?
Jedis is a Java client for Redis designed for performance and ease of use.
Are you looking for a high-level library to handle object mapping? See redis-om-spring!
How do I Redis?
Learn for free at Redis University
Supported Redis versions
The most recent version of this library supports redis version
7.2,
7.4,
8.0,
8.2,
8.4,
8.6,
8.8 and
8.10.
The table below highlights version compatibility of the most-recent library versions with Redis and JDK versions. Compatibility means communication features, and Redis command capabilities.
| Jedis version | Supported Redis versions | JDK Compatibility |
|---|---|---|
| 3.9+ | 5.0 to 6.2 Family of releases | 8, 11 |
| >= 4.0 | Version 5.0 to 7.2 Family of releases | 8, 11, 17 |
| >= 5.0 | Version 6.0 to current | 8, 11, 17, 21 |
| >= 5.2 | Version 7.2 to current | 8, 11, 17, 21 |
| >= 6.0 | Version 7.2 to current | 8, 11, 17, 21 |
| >= 7.0 | Version 7.2 to current | 8, 11, 17, 21 |
| >= 8.0 | Version 7.2 to current | 8, 11, 17, 21, 25 |
Getting started
To get started with Jedis, first add it as a dependency in your Java project. If you're using Maven, that looks like this:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>8.0.1</version>
</dependency>To use the cutting-edge Jedis, check here.
Next, you'll need to connect to Redis. Consider installing a redis server with docker:
docker run -p 6379:6379 -it redis:latestYou can instantiate a RedisClient like so:
RedisClient jedis = RedisClient.builder().hostAndPort("localhost", 6379).build();Now you can send commands:
jedis.sadd("planets", "Venus");Connecting to a Redis cluster
Jedis lets you connect to Redis Clusters, supporting the Redis Cluster Specification.
To do this, you'll need to connect using RedisClusterClient. See the example below:
Set<HostAndPort> jedisClusterNodes = new HashSet<HostAndPort>();
jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7379));
jedisClusterNodes.add(new HostAndPort("127.0.0.1", 7380));
RedisClusterClient jedis = RedisClusterClient.builder().nodes(jedisClusterNodes).build();Now you can use the RedisClusterClient instance and send commands like you would with a standard pooled connection:
jedis.sadd("planets", "Mars");Support for Redis data types
Jedis includes support for all Redis data types and features such as
JSON and VectorSets.
Failover
Jedis supports retry and failover for your Redis deployments. This is useful when:
- You have more than one Redis deployment. This might include two independent Redis servers or two or more Redis databases replicated across multiple active-active Redis Enterprise clusters.
- You want your application to connect to one deployment at a time and to fail over to the next available deployment if the first deployment becomes unavailable.
For the complete failover configuration options and examples, see the Jedis failover docs.
Token-Based Authentication
Jedis supports Token-Based authentication (TBA) starting with 5.3.0 GA release. This feature is complemented by an extension library that enhances the developer experience and provides most of the components required for TBA functionality.
Notably, the extension library includes built-in support for Microsoft EntraID, offering a seamless integration as part of the generic solution.
For more details and examples, please refer to the Advanced Usage documentation.
Documentation
The Jedis documentation site contains several useful articles for using Jedis.
You can also check the latest Jedis Javadocs.
Some specific use-case examples can be found in redis.clients.jedis.examples
package of the test source codes.
Troubleshooting
If you run into trouble or have any questions, we're here to help!
Hit us up on the Redis Discord Server or
Jedis GitHub Discussions.
Contributing
We'd love your contributions!
Bug reports are always welcome! You can open a bug report on GitHub.
To report a security vulnerability, please follow our Security Policy.
You can also contribute documentation -- or anything to improve Jedis. Please see
contribution guideline for more details.
License
Jedis is licensed under the MIT license.
Sponsorship
Overview (`docs/README.md`)
Jedis Documentation
This documentation uses MkDocs to generate the static site.
See mkdocs.yml for the configuration.
To develop the documentation locally, you can use the included Dockerfile to build a container with all the
dependencies, and run it to preview your changes:
# in docs/
docker build -t squidfunk/mkdocs-material .
# cd ..
docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-materialOverview (`src/test/java/redis/clients/jedis/codegen/README.md`)
Code Generators
This package contains code generation tools for the Jedis project. These are not tests and should not be executed as part of the test suite.
CommandFlagsRegistryGenerator
Automatically generates and updates the static flags registry in CommandObject.java by retrieving command metadata from a running Redis server.
Purpose
The CommandObject class uses a static registry to map Redis command names to their flags.
How It Works
- Connects to a Redis server (default: localhost:6379)
- Retrieves all command metadata using the
COMMANDcommand - Processes commands and subcommands, extracting their flags
- Groups commands by their flag combinations
- Generates a static initializer block with inline
EnumSetcreation - Updates
CommandObject.javaautomatically using regex pattern matching - Saves a backup JSON file for offline use
Prerequisites
- A running Redis server (version 7.0+ recommended for full command metadata)
- The Redis server should have all modules loaded if you want to include module commands
When to Run
Run this generator whenever:
- Upgrading to a new Redis version
- New Redis modules are added to your server
- Command flags are modified in Redis
- You want to ensure the registry is up-to-date with your Redis server
Fallback Mode
If the generator cannot connect to Redis, it will automatically fall back to using the backup JSON file (redis_commands_flags.json) if available.
Output
The generator will:
- ✓ Connect to Redis and retrieve command metadata
- ✓ Process commands and subcommands
- ✓ Group commands by flag combinations
- ✓ Generate the complete static initializer block
- ✓ Update
src/main/java/redis/clients/jedis/CommandObject.javain-place - ✓ Save a backup JSON file for offline use
- ✓ Preserve original command names (with spaces, dots, hyphens)
Overview (`src/test/jmh/redis/clients/jedis/benchmark/README.md`)
Jedis JMH Benchmarks
JMH (Java Microbenchmark Harness) benchmarks for Jedis performance testing.
Quick Start
Maven (CI/CD)
mvn -Pjmh clean test # All benchmarks
mvn -Pjmh test -Djmh.includes="CRC16Benchmark" # Specific suiteIDE (Development)
- Open
JmhMain.java - Uncomment desired suite:
runAllBenchmarks(),runCRC16Benchmarks(), etc. - Right-click → Run
Results
benchmarks.json- Machine-readable resultsbenchmark.log- Execution log
Available Benchmarks
protocol.ReadBenchmark (10 benchmarks)
RESP3 Protocol.read operations with optional client-side cache.
Categories:
- Baseline reads (no cache):
readSimpleString,readBulkString,readArray,readMultiBulkResponse - Cache-aware overhead:
cacheAwareRead*variants (push-check path, no pushes present) - Mixed scenarios:
readWith1PushMessage,readWith100PushMessages(push frames preceding a response)
Use case: Measure RESP parsing cost and the overhead introduced by client-side caching.
protocol.ReadPushesBenchmark (2 benchmarks)
Protocol.readPushes — client-side cache invalidation processing.
Methods:
drain1Pending- OnereadPushescall drains 1 pending push framedrain1000Pending- OnereadPushescall drains 1000 pending push frames
Use case: Measure how readPushes per-call cost scales with burst size; reveals fixed-vs-amortized overhead.
protocol.SendCommandBenchmark (1 benchmark)
Protocol.sendCommand — RESP command encoding.
Methods:
measureSendCommand- Encode SET command (full path:CommandArguments+ encode)
Use case: Measure command-encoding cost without I/O.
CRC16Benchmark (2 benchmarks)
Redis Cluster hash slot calculation.
Methods:
getSlotString- String-based calculationgetSlotBytes- byte[]-based calculation
Test data: Rotates through 8 key patterns (empty, short, long, hashtag, etc.)
Batch size: 128 ops (8 patterns × 16)
SafeEncoderBenchmark (3 benchmarks)
UTF-8 encoding/decoding performance.
Methods:
encodeStringToBytes- String → byte[]decodeBytesToString- byte[] → StringencodeMany- Bulk encoding
Test data: Rotates through 6 string patterns (2-44 chars)
Batch size: 120 ops (6 patterns × 20)
jedis.GetSetBenchmark (4 benchmarks) ⚠️ Requires Redis
Jedis GET/SET operations over network.
Methods:
set- Write performance (foo=bar)get- Read performance (foo=bar)pipelinedSet- Pipelined write (1000 ops, sync every 100)pipelinedGet- Pipelined read (1000 ops, sync every 100)
Requirements: Redis 6.0+ running on localhost:6379 (or configured endpoint)
Mode: Throughput (ops/sec)
redisclient.GetSetBenchmark (12 benchmarks) ⚠️ Requires Redis
RedisClient (pooled) GET/SET operations with concurrency testing.
Thread configurations:
redisclient.GetSetBenchmark$Threads1- 1 thread (baseline)redisclient.GetSetBenchmark$Threads8- 8 threads (moderate concurrency)redisclient.GetSetBenchmark$Threads64- 64 threads (high concurrency)
Methods per configuration:
set- Write performance using pooled RedisClientget- Read performance using pooled RedisClientpipelinedSet- Pipelined write (1000 ops, sync every 100)pipelinedGet- Pipelined read (1000 ops, sync every 100)
Pool configuration:
- MaxTotal: 64 connections (sized for max thread count)
- MaxIdle: 64, MinIdle: 8
- Prevents pool contention from skewing results
Workload:
- Same single key/value pair (
"foo"/"bar") asjedis.GetSetBenchmarkfor direct comparability - 1-thread variant produces numbers comparable to
jedis.GetSetBenchmark - 8/64-thread variants exercise pool concurrency on the same workload
Requirements: Redis 6.0+ running on localhost:6379 (or configured endpoint)
Mode: Throughput (ops/sec)
Purpose: Test connection pool efficiency and pipelining under different load levels
Configuration
JMH Settings:
- Forks: 1
- Warmup: 3 iterations × 1s
- Measurement: 5 iterations × 1s
- Output: Average time (ns/op)
Understanding Results
Benchmark Mode Cnt Score Error Units
CRC16Benchmark.getSlotString avgt 5 47.123 ± 2.456 ns/op- Score: Average time per operation (lower = better)
- Error: 99.9% confidence interval (< 10% = good)
- Units: ns/op (nanoseconds per operation)
Good result: Error < 10% of score
High variance: Error > 20% of score (need more iterations or batching)
Notes
- Batching: Fast operations (< 100 ns) use batching to reduce JMH overhead
- Test patterns: Batch size is always a multiple of pattern count for consistent results
- IDE runner:
JmhMain.javaprovides easy IDE execution - Redis required:
jedis.GetSetBenchmarkandredisclient.GetSetBenchmarkrequire a running Redis instance (localhost:6379) - CI/CD: GitHub Actions runs benchmarks nightly, results published to gh-pages
Redis Client Components Overview (`docs/redis-client-components-overview.md`)
Redis Client Components Overview
Overview
This document provides a high-level overview of the Jedis client architecture, focusing on RedisClient, RedisClusterClient, RedisSentinelClient, and the experimental MultiDbClient. The architecture is built on two core abstractions:
- ConnectionProvider - Manages connections to Redis servers
- CommandExecutor - Executes Redis commands with various strategies (retry, cluster routing, circuit breaker)
All client implementations extend UnifiedJedis, which delegates command execution to a CommandExecutor that uses a ConnectionProvider to obtain connections.
Architecture Diagram
flowchart TB
subgraph ClientLayer["Client Layer"]
RC["RedisClient<br/>(Standalone)"]
RCC["RedisClusterClient<br/>(Cluster)"]
RSC["RedisSentinelClient<br/>(Sentinel)"]
MDC["MultiDbClient<br/>(Multi-DB Failover)"]
end
UJ["UnifiedJedis<br/><br/>- executor<br/>- provider<br/>- cache"]
CE["CommandExecutor"]
CP["ConnectionProvider"]
RC --> UJ
RCC --> UJ
RSC --> UJ
MDC --> UJ
UJ --> CE
UJ --> CPCore Components
UnifiedJedis
Location: src/main/java/redis/clients/jedis/UnifiedJedis.java
Responsibilities:
- Base implementation class for all Redis clients
- Implements all Redis command interfaces
- Delegates command execution to
CommandExecutor - Manages client-side caching (CSC) when enabled
Key Fields:
protected final ConnectionProvider provider;
protected final CommandExecutor executor;
protected final CommandObjects commandObjects;
private final Cache cache;Key Methods:
- All Redis commands (GET, SET, HGET, etc.) - delegates to
executor.executeCommand() close()- closes the executor (which closes the provider)
CommandExecutor Hierarchy
Interface: CommandExecutor
Location: src/main/java/redis/clients/jedis/executors/CommandExecutor.java
public interface CommandExecutor extends AutoCloseable {
<T> T executeCommand(CommandObject<T> commandObject);
}Implementations
SimpleCommandExecutor
Location: src/main/java/redis/clients/jedis/executors/SimpleCommandExecutor.java
Characteristics:
- Simplest implementation
- Uses a single
Connectiondirectly - No retry logic
- No connection pooling
- Use Case: the deprecated
UnifiedJedis(Connection)constructor (single fixed
connection). Note:PipelineandTransactiondo not use aCommandExecutorat
all — they queue commands directly on theirConnection.
Execution Flow:
executeCommand() → connection.executeCommand()DefaultCommandExecutor
Location: src/main/java/redis/clients/jedis/executors/DefaultCommandExecutor.java
Characteristics:
- Uses
ConnectionProviderto get connections - Connection pooling support
- No retry logic
- Use Case: RedisClient (standalone) without retry settings
Execution Flow:
executeCommand() → provider.getConnection() → connection.executeCommand() → close connectionRetryableCommandExecutor
Location: src/main/java/redis/clients/jedis/executors/RetryableCommandExecutor.java
Characteristics:
- Retry logic with deadline-proportional backoff (the remaining time budget is
spread across the remaining attempts) - Configurable
maxAttemptsandmaxTotalRetriesDuration - Handles
JedisConnectionExceptionwith backoff (sleeps only after 2+
consecutive connection failures) - Deadline-based retry termination
- Use Case: wired via the
UnifiedJedis(provider, maxAttempts, maxTotalRetriesDuration)constructor. Not currently created byRedisClient's builder (seeClusterClientBuilderfor builder-level retry
settings).
Configuration:
RetryableCommandExecutor(provider, maxAttempts, maxTotalRetriesDuration)Execution Flow:
executeCommand()
├─ for (attempts = maxAttempts; attempts > 0; attempts--)
│ ├─ provider.getConnection()
│ ├─ connection.executeCommand()
│ └─ catch JedisConnectionException
│ ├─ log failure
│ ├─ backoff sleep (after 2+ consecutive connection failures)
│ └─ retry if deadline not exceeded
└─ throw if all attempts exhaustedBackoff Formula:
sleepMillis = millisLeft / (attemptsLeft * (attemptsLeft + 1))ClusterCommandExecutor
Location: src/main/java/redis/clients/jedis/executors/ClusterCommandExecutor.java
Characteristics:
- Cluster-aware routing based on hash slots
- Handles MOVED/ASK redirections
- Supports broadcast commands (ALL_SHARDS, ALL_NODES)
- Round-robin distribution for keyless commands
- Retry logic with cluster topology awareness
- Replica connection support
- Use Case: RedisClusterClient
Configuration:
ClusterCommandExecutor(provider, maxAttempts, maxTotalRetriesDuration, flags)Request Policies:
DEFAULT- Single-shard routing based on key hash slotALL_SHARDS- Broadcast to all primary nodesALL_NODES- Broadcast to all nodes (including replicas)MULTI_SHARD- Multi-key commands spanning shardsSPECIAL- Special handling (SCAN, FT.CURSOR, etc.)
Execution Flow:
executeCommand()
├─ Determine request policy from command flags
├─ ALL_SHARDS → broadcastCommand(primaryOnly=true)
├─ ALL_NODES → broadcastCommand(primaryOnly=false)
└─ DEFAULT
├─ if keyless → executeKeylessCommand() (round-robin)
└─ else → doExecuteCommand() (slot-based routing)
├─ Calculate hash slot from key
├─ provider.getConnection(slot)
├─ connection.executeCommand()
└─ Handle MOVED/ASK redirections
├─ MOVED → update slot cache, retry
└─ ASK → send ASKING, execute on targetKey Features:
- Slot-based routing: Uses CRC16 hash to determine target shard
- Redirection handling: Follows MOVED (permanent) and ASK (temporary) redirects
- Topology refresh: Updates cluster topology on MOVED responses
- Round-robin: Distributes keyless commands across cluster nodes
MultiDbCommandExecutor
Location: src/main/java/redis/clients/jedis/mcf/MultiDbCommandExecutor.java
Characteristics:
- Circuit breaker pattern (Resilience4j)
- Retry logic with exponential backoff
- Multi-database failover (DR, backup, active-active)
- Automatic failover on circuit breaker OPEN state
- Health check integration
- Experimental feature
- Use Case: MultiDbClient for high availability
Configuration:
MultiDbCommandExecutor(multiDbConnectionProvider)Execution Flow:
executeCommand()
├─ database = provider.getDatabase() (active database)
├─ Decorate with Resilience4j:
│ ├─ withCircuitBreaker(database.circuitBreaker)
│ ├─ withRetry(database.retry)
│ └─ withFallback(exceptions, failoverHandler)
├─ Execute: handleExecuteCommand()
│ ├─ database.getConnection()
│ ├─ connection.executeCommand()
│ └─ catch exceptions tracked by circuit breaker
└─ On circuit breaker OPEN
├─ databaseFailover() (switch to next healthy database)
└─ retry on new databaseCircuit Breaker States:
CLOSED- Normal operation, tracking failuresOPEN- Too many failures, reject requests, trigger failoverHALF_OPEN- Testing if database recovered
Failover Strategy:
- Weighted selection of healthy databases
- Automatic failback to higher-priority databases
- Health check probing (PING or lag-aware strategies)
ConnectionProvider Hierarchy
Interface: ConnectionProvider
Location: src/main/java/redis/clients/jedis/providers/ConnectionProvider.java
public interface ConnectionProvider extends AutoCloseable {
Connection getConnection();
Connection getConnection(CommandArguments args);
Map<?, ?> getConnectionMap();
Map<?, ?> getPrimaryNodesConnectionMap();
}Implementations
ManagedConnectionProvider
Location: src/main/java/redis/clients/jedis/providers/ManagedConnectionProvider.java
Characteristics:
- Single managed connection
- No pooling
- Connection set externally
- Use Case: supplying an externally managed
ConnectiontoUnifiedJedis
(advanced/custom wiring). Note:PipelineandTransactionreceive theirConnectiondirectly, without a provider.
Key Methods:
setConnection(Connection connection) // Set the managed connection
getConnection() → connection // Return the same connectionPooledConnectionProvider
Location: src/main/java/redis/clients/jedis/providers/PooledConnectionProvider.java
Characteristics:
- Apache Commons Pool2 for connection pooling
- Configurable pool settings (maxTotal, maxIdle, minIdle)
- Connection validation
- Use Case: RedisClient (standalone)
Configuration:
PooledConnectionProvider(hostAndPort, clientConfig, poolConfig)Key Methods:
getConnection() → pool.getResource()
getConnection(args) → pool.getResource() // Ignores args for standalonePool Configuration:
maxTotal- Maximum connections in poolmaxIdle- Maximum idle connectionsminIdle- Minimum idle connectionstestOnBorrow- Validate connection before usetestWhileIdle- Validate idle connections
ClusterConnectionProvider
Location: src/main/java/redis/clients/jedis/providers/ClusterConnectionProvider.java
Characteristics:
- Cluster-aware connection management
- Slot-based routing (16384 slots)
- Connection pool per cluster node
- Automatic topology refresh
- Replica connection support
- Use Case: RedisClusterClient
Configuration:
ClusterConnectionProvider(clusterNodes, clientConfig, poolConfig)Key Methods:
getConnection(CommandArguments args)
├─ Extract hash slots from keys
├─ Determine target slot
└─ getConnectionFromSlot(slot) → pool for that slot's primary node
getConnection(HostAndPort node)
└─ Get connection to specific node
getReplicaConnection(CommandArguments args)
└─ Get connection to replica for read operationsSlot Mapping:
- Maintains mapping:
slot → HostAndPort → ConnectionPool - CRC16 hash algorithm:
slot = CRC16(key) % 16384 - Handles hash tags:
{user}:123→ hash onlyuser
Topology Management:
- Initial topology discovery via
CLUSTER SLOTSorCLUSTER NODES - Periodic refresh based on configuration
- On-demand refresh on MOVED redirections
SentineledConnectionProvider
Location: src/main/java/redis/clients/jedis/providers/SentineledConnectionProvider.java
Characteristics:
- Sentinel-aware connection management
- Automatic master failover detection
- Listens to sentinel events (+switch-master)
- Connection pool to current master
- Use Case: RedisSentinelClient
Configuration:
SentineledConnectionProvider(masterName, masterClientConfig, poolConfig,
sentinels, sentinelClientConfig)Key Methods:
getConnection() → pool.getResource() // Connection to current master
getCurrentMaster() → HostAndPort // Current master endpointFailover Handling:
- Subscribes to sentinel pub/sub channels
- Listens for
+switch-masterevents - Automatically recreates pool on master change
- Validates new master before switching
MultiDbConnectionProvider
Location: src/main/java/redis/clients/jedis/mcf/MultiDbConnectionProvider.java
Characteristics:
- Multiple database endpoints with isolated pools
- Weighted database selection
- Circuit breaker per database (Resilience4j)
- Health check integration (PING, lag-aware)
- Automatic failover and failback
- Database switch event listeners
- Experimental feature
- Use Case: MultiDbClient for DR/backup/active-active
Configuration:
MultiDbConnectionProvider(multiDbConfig)
├─ DatabaseConfig[] - array of database endpoints
├─ RetryConfig - retry settings per database
├─ CircuitBreakerConfig - failure detection settings
├─ HealthCheckStrategy - PING or lag-aware
└─ Failback settingsKey Concepts:
Database:
- Each database has:
TrackingConnectionPool- connection poolCircuitBreaker- failure trackingRetry- retry configurationweight- selection priority (higher weight preferred)HealthCheck- health monitoring
Active Database Selection:
getDatabase() → activeDatabase (highest weight healthy database)Failover Process:
1. Circuit breaker detects failures
2. Circuit breaker transitions to OPEN state
3. MultiDbCommandExecutor triggers failover
4. Select next healthy database by weight
5. Switch activeDatabase
6. Emit DatabaseSwitchEvent
7. Continue operations on new databaseFailback Process:
1. Periodic health check (background thread)
2. Detect higher-priority database is healthy
3. Automatic failback to higher-priority database
4. Emit DatabaseSwitchEventHealth Check Strategies:
PingStrategy- Simple PING commandLagAwareStrategy- Check replication lag (for active-active)
Key Methods:
getDatabase() → Database // Get active database
getConnection() → activeDatabase.getConnection()
setActiveDatabase(endpoint) // Manual failover
setDatabaseSwitchListener(listener) // Listen to switch eventsClient Implementations
RedisClient (Standalone)
Location: src/main/java/redis/clients/jedis/RedisClient.java
Builder: StandaloneClientBuilder
Default Configuration:
ConnectionProvider: PooledConnectionProvider
CommandExecutor: DefaultCommandExecutorUsage:
RedisClient client = RedisClient.builder()
.hostAndPort("localhost", 6379)
.clientConfig(DefaultJedisClientConfig.builder()
.password("secret")
.build())
.poolConfig(poolConfig)
.build();
String value = client.get("key");
client.close();Retries: builder-level retry settings (maxAttempts,maxTotalRetriesDuration) are currently exposed only on ClusterClientBuilder.
For standalone retries, construct UnifiedJedis(provider, maxAttempts, maxTotalRetriesDuration) directly, which wires a RetryableCommandExecutor.
RedisClusterClient
Location: src/main/java/redis/clients/jedis/RedisClusterClient.java
Builder: ClusterClientBuilder
Default Configuration:
ConnectionProvider: ClusterConnectionProvider
CommandExecutor: ClusterCommandExecutorUsage:
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("localhost", 7000));
nodes.add(new HostAndPort("localhost", 7001));
RedisClusterClient client = RedisClusterClient.builder()
.nodes(nodes)
.maxAttempts(5)
.maxTotalRetriesDuration(Duration.ofSeconds(30))
.build();
String value = client.get("key");
client.close();Cluster-Specific Features:
- Automatic slot-based routing
- MOVED/ASK redirection handling
- Broadcast commands (FLUSHALL, SCRIPT LOAD)
- Multi-key operations validation
RedisSentinelClient
Location: src/main/java/redis/clients/jedis/RedisSentinelClient.java
Builder: SentinelClientBuilder
Default Configuration:
ConnectionProvider: SentineledConnectionProvider
CommandExecutor: DefaultCommandExecutorUsage:
Set<HostAndPort> sentinels = new HashSet<>();
sentinels.add(new HostAndPort("localhost", 26379));
sentinels.add(new HostAndPort("localhost", 26380));
RedisSentinelClient client = RedisSentinelClient.builder()
.masterName("mymaster")
.sentinels(sentinels)
.build();
String value = client.get("key");
client.close();Sentinel-Specific Features:
- Master discovery through sentinels
- Automatic reconnection to the new master on
+switch-master - Separate
sentinelClientConfig(...)for connecting to the sentinel nodes
MultiDbClient (Experimental)
Location: src/main/java/redis/clients/jedis/MultiDbClient.java
Builder: MultiDbClientBuilder
Default Configuration:
ConnectionProvider: MultiDbConnectionProvider
CommandExecutor: MultiDbCommandExecutorUsage:
DatabaseConfig primary = DatabaseConfig.builder(
new HostAndPort("primary.redis.com", 6379), clientConfig)
.weight(100.0f) // Highest priority
.build();
DatabaseConfig dr = DatabaseConfig.builder(
new HostAndPort("dr.redis.com", 6379), clientConfig)
.weight(50.0f) // Lower priority
.build();
MultiDbConfig multiDbConfig = MultiDbConfig.builder()
.database(primary)
.database(dr)
.failureDetector(CircuitBreakerConfig.builder()
.failureRateThreshold(50.0f)
.build())
.commandRetry(RetryConfig.builder()
.maxAttempts(3)
.build())
.failbackCheckInterval(30_000) // milliseconds
.build();
MultiDbClient client = MultiDbClient.builder()
.multiDbConfig(multiDbConfig)
.databaseSwitchListener(event -> {
System.out.println("Switched to: " + event.getEndpoint());
})
.build();
String value = client.get("key"); // Automatically fails over on errors
client.close();Multi-DB Features:
- Circuit breaker per database
- Automatic failover on failures
- Automatic failback to higher-priority databases
- Health check monitoring
- Database switch event notifications
Builder Pattern Hierarchy
AbstractClientBuilder
Location: src/main/java/redis/clients/jedis/builders/AbstractClientBuilder.java
Common Configuration:
clientConfig- JedisClientConfig (auth, SSL, timeout, protocol)poolConfig- GenericObjectPoolConfig (pool settings)cacheConfig/cache- Client-side caching configurationconnectionProvider/commandExecutor- custom component overrides
(Retry settings maxAttempts / maxTotalRetriesDuration are declared onClusterClientBuilder, not on the abstract base.)
Template Methods:
// Abstract — every builder must implement these
protected abstract T self();
protected abstract ConnectionProvider createDefaultConnectionProvider();
protected abstract C createClient();
protected abstract void validateSpecificConfiguration();
// Concrete — defaults to DefaultCommandExecutor; overridden by
// ClusterClientBuilder and MultiDbClientBuilder
protected CommandExecutor createDefaultCommandExecutor();Builder Hierarchy:
AbstractClientBuilder
├─ StandaloneClientBuilder → RedisClient
├─ ClusterClientBuilder → RedisClusterClient
├─ SentinelClientBuilder → RedisSentinelClient
└─ MultiDbClientBuilder → MultiDbClientExecution Flow Examples
Example 1: Simple GET Command (RedisClient)
client.get("mykey")
↓
UnifiedJedis.get("mykey")
↓
executor.executeCommand(CommandObject<String>)
↓
DefaultCommandExecutor.executeCommand()
├─ provider.getConnection()
│ ↓
│ PooledConnectionProvider.getConnection()
│ └─ pool.getResource() → Connection
├─ connection.executeCommand(GET mykey)
└─ connection.close() (return to pool)
↓
Return "value"Example 2: GET Command with Retry (UnifiedJedis with RetryableCommandExecutor)
client.get("mykey")
↓
UnifiedJedis.get("mykey")
↓
executor.executeCommand(CommandObject<String>)
↓
RetryableCommandExecutor.executeCommand()
├─ Attempt 1:
│ ├─ provider.getConnection()
│ ├─ connection.executeCommand(GET mykey)
│ └─ JedisConnectionException thrown
├─ Backoff sleep (after 2+ consecutive connection failures)
├─ Attempt 2:
│ ├─ provider.getConnection()
│ ├─ connection.executeCommand(GET mykey)
│ └─ Success!
└─ Return "value"Example 3: GET Command in Cluster (RedisClusterClient)
client.get("mykey")
↓
UnifiedJedis.get("mykey")
↓
executor.executeCommand(CommandObject<String>)
↓
ClusterCommandExecutor.executeCommand()
├─ Calculate slot: CRC16("mykey") % 16384 = 14687
├─ provider.getConnection(args)
│ ↓
│ ClusterConnectionProvider.getConnection(args)
│ ├─ Extract slot from args: 14687
│ ├─ Lookup node for slot: 192.168.1.10:7000
│ └─ cache.getPool(node).getResource() → Connection
├─ connection.executeCommand(GET mykey)
├─ Receive MOVED 14687 192.168.1.11:7001
├─ Update slot cache
├─ provider.getConnection(192.168.1.11:7001)
├─ connection.executeCommand(GET mykey)
└─ Return "value"Example 4: GET Command with Multi-DB Failover (MultiDbClient)
client.get("mykey")
↓
UnifiedJedis.get("mykey")
↓
executor.executeCommand(CommandObject<String>)
↓
MultiDbCommandExecutor.executeCommand()
├─ database = provider.getDatabase() → primary (weight=1.0)
├─ Decorate with Resilience4j:
│ ├─ CircuitBreaker (state=CLOSED)
│ ├─ Retry (maxAttempts=3)
│ └─ Fallback (on circuit breaker OPEN)
├─ Attempt 1 on primary:
│ ├─ database.getConnection()
│ ├─ connection.executeCommand(GET mykey)
│ └─ JedisConnectionException (primary is down!)
├─ Circuit breaker records failure
├─ Retry attempt 2 on primary:
│ └─ JedisConnectionException
├─ Circuit breaker records failure
├─ Retry attempt 3 on primary:
│ └─ JedisConnectionException
├─ Circuit breaker transitions to OPEN
├─ Fallback triggered:
│ ├─ databaseFailover()
│ ├─ Select next healthy database → dr (weight=0.5)
│ ├─ setActiveDatabase(dr)
│ └─ Emit DatabaseSwitchEvent
├─ Recursive call: executeCommand() on dr
│ ├─ database.getConnection()
│ ├─ connection.executeCommand(GET mykey)
│ └─ Success!
└─ Return "value"Advanced Usage (`docs/advanced-usage.md`)
Advanced Usage
Transactions
To do transactions in Jedis, you have to wrap operations in a transaction block, very similar to pipelining:
jedis.watch (key1, key2, ...);
Transaction t = jedis.multi();
t.set("foo", "bar");
t.exec();Note: when you have any method that returns values, you have to do like this:
Transaction t = jedis.multi();
t.set("fool", "bar");
Response<String> result1 = t.get("fool");
t.zadd("foo", 1, "barowitch"); t.zadd("foo", 0, "barinsky"); t.zadd("foo", 0, "barikoviev");
Response<Set<String>> sose = t.zrange("foo", 0, -1); // get the entire sortedset
t.exec(); // dont forget it
String foolbar = result1.get(); // use Response.get() to retrieve things from a Response
int soseSize = sose.get().size(); // on sose.get() you can directly call Set methods!
// List<Object> allResults = t.exec(); // you could still get all results at once, as beforeNote that a Response Object does not contain the result before t.exec() is called (it is a kind of a Future). Forgetting exec gives you exceptions. In the last lines, you see how transactions/pipelines were dealt with before version 2. You can still do it that way, but then you need to extract objects from a list, which contains also Redis status messages.
Note 2: Redis does not allow to use intermediate results of a transaction within that same transaction. This does not work:
// this does not work! Intra-transaction dependencies are not supported by Redis!
jedis.watch(...);
Transaction t = jedis.multi();
if(t.get("key1").equals("something"))
t.set("key2", "value2");
else
t.set("key", "value");However, there are some commands like setnx, that include such a conditional execution. Those are of course supported within transactions. You can build your own customized commands using EVAL / LUA scripting.
Pipelining
Sometimes you need to send a bunch of different commands. A very cool way to do that, and have better performance than doing it the naive way, is to use pipelining. This way you send commands without waiting for response, and you actually read the responses at the end, which is faster.
Here is how to do it:
Pipeline p = jedis.pipelined();
p.set("fool", "bar");
p.zadd("foo", 1, "barowitch"); p.zadd("foo", 0, "barinsky"); p.zadd("foo", 0, "barikoviev");
Response<String> pipeString = p.get("fool");
Response<Set<String>> sose = p.zrange("foo", 0, -1);
p.sync();
int soseSize = sose.get().size();
Set<String> setBack = sose.get();For more explanations see code comments in the transaction section.
Avoiding pool waits with cluster pipelines
A cluster pipeline keeps one connection borrowed from every node it targets until sync() or close(). Concurrent pipelines that target nodes in different orders can exhaust small per-node pools and wait on one another indefinitely when the pool uses its default unbounded wait.
For workloads with concurrent multi-node pipelines, use a dedicated cluster client. Size each node pool for the expected number of concurrent pipelines and configure a finite maxWait so pool exhaustion fails within a known interval instead of waiting indefinitely:
ConnectionPoolConfig pipelinePoolConfig = new ConnectionPoolConfig();
pipelinePoolConfig.setMaxTotal(expectedConcurrentPipelines);
pipelinePoolConfig.setMaxWait(Duration.ofSeconds(1));
try (RedisClusterClient pipelineClient = RedisClusterClient.builder()
.nodes(nodes)
.clientConfig(clientConfig)
.poolConfig(pipelinePoolConfig)
.build();
ClusterPipeline pipeline = pipelineClient.pipelined()) {
// Append commands, then call sync() before reading responses.
}Keep each pipeline single-shard when possible. This avoids borrowing multiple node connections at the same time.
Publish/Subscribe
To subscribe to a channel in Redis, create an instance of JedisPubSub and call subscribe on the Jedis instance:
class MyListener extends JedisPubSub {
public void onMessage(String channel, String message) {
}
public void onSubscribe(String channel, int subscribedChannels) {
}
public void onUnsubscribe(String channel, int subscribedChannels) {
}
public void onPSubscribe(String pattern, int subscribedChannels) {
}
public void onPUnsubscribe(String pattern, int subscribedChannels) {
}
public void onPMessage(String pattern, String channel, String message) {
}
}
MyListener l = new MyListener();
jedis.subscribe(l, "foo");Note that subscribe is a blocking operation because it will poll Redis for responses on the thread that calls subscribe. A single JedisPubSub instance can be used to subscribe to multiple channels. You can call subscribe or psubscribe on an existing JedisPubSub instance to change your subscriptions.
Monitoring
To use the monitor command you can do something like the following:
new Thread(new Runnable() {
public void run() {
Jedis j = new Jedis("localhost");
for (int i = 0; i < 100; i++) {
j.incr("foobared");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
}
j.disconnect();
}
}).start();
jedis.monitor(new JedisMonitor() {
public void onCommand(String command) {
System.out.println(command);
}
});Token-Based Authentication
Starting with version 5.3.0 GA, Jedis supports token-based authentication. The redis-authx-entraid repository provides the necessary components that Jedis utilizes to enable this functionality.
Additionally, support for Microsoft EntraID has been fully implemented and is now available as an extension for Azure Managed Redis (AMR) and Azure Cache for Redis(ACR).
Using With Custom Identity Provider
Jedis provides a token-based authentication mechanism with a generic identity provider of your choice.
For custom use of this feature, you will need to provide an implementation of IdentityProvider and IdentityProviderConfig and configure it in the way Jedis expects.
You will have the required interfaces from transitive Jedis dependencies;
<dependency>
<groupId>redis.clients.authentication</groupId>
<artifactId>redis-authx-core</artifactId>
<version>${version}</version>
</dependency>An example to get started:
public class YourCustomIdentityProviderConfig implements IdentityProviderConfig {
...
}
public class YourCustomIdentityProvider implements IdentityProvider {
...
}Then configure Jedis like this:
IdentityProviderConfig yourCustomIdentityProviderConfig = new YourCustomIdentityProviderConfig();
TokenAuthConfig tokenAuthConfig = TokenAuthConfig.builder().identityProviderConfig(yourCustomIdentityProviderConfig);
JedisClientConfig config = DefaultJedisClientConfig.builder()
.authXManager(new AuthXManager(tokenAuthConfig)).build();
...Using With Microsoft EntraID
Extension for EntraID is fully integrated and ready to use with Azure Managed Redis(AMR) or Azure Cache for Redis(ACR). All you need is to add the EntraID dependency and code for configuration for chosen authentication type with Microsoft EntraID service.
To get started, add the redis-authx-entraid extension as dependency;
<dependency>
<groupId>redis.clients.authentication</groupId>
<artifactId>redis-authx-entraid</artifactId>
<version>${version}</version>
</dependency>After adding the dependency, configure it using EntraIDTokenAuthConfigBuilder:
...
TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder()
.expirationRefreshRatio(0.8F)
.clientId("yourClientId")
.secret("yourClientSecret")
.authority("yourAuthority")
.scopes("yourRedisScopes").build();
AuthXManager authXManager = new AuthXManager(tokenAuthConfig);
JedisClientConfig config = DefaultJedisClientConfig.builder()
.authXManager(authXManager).build();
...Here you will see the AuthXManager class that is built into Jedis. Essentially it integrates the extension into Jedis and handles the authentication process.
Lifecycle: you create the AuthXManager, so you own it, just like the JedisClientConfig that carries it. Jedis starts it when the first client configured with it is created, and one manager may be shared by any number of clients. Jedis never stops it: call authXManager.stop() yourself once the last client using it has been closed, otherwise its token renewal threads keep running.
For other available configurations, detailed information and usage of Jedis with Microsoft EntraID, please refer to the official guide
Setting Up AMR or ACR with Microsoft EntraID:
To use Microsoft EntraID with AMR or ACR, for sure you will need to set up and configure your AMR/ACR services as well as Microsoft EntraID. The following resources provide useful information;
Microsoft Entra ID for AMR authentication
Microsoft Entra ID for ACR authentication
HostAndPortMapper
When running Jedis in certain network environments, such as behind a NAT gateway, or within container orchestration systems like Docker or Kubernetes, the host and port that the client needs to connect to might be different from the host and port that the Redis Cluster nodes report. To handle this discrepancy, Jedis provides the HostAndPortMapper interface.
This allows you to dynamically map the address reported by a Redis node to a different address that the client can actually reach. You can implement this either by creating a dedicated class or by using a concise lambda expression.
Example use case: NAT or Docker with different advertised ports
Suppose you run a Redis cluster inside Docker on a remote host.
Inside the cluster config, nodes announce addresses like:
172.18.0.2:6379
172.18.0.3:6379
172.18.0.4:6379But externally, you reach them through the host IP with mapped ports:
my-redis.example.com:7001
my-redis.example.com:7002
my-redis.example.com:7003Implementing with a Dedicated Class
You can provide your mapping logic by creating a class that implements the HostAndPortMapper interface. This approach is useful for more complex mapping logic or for reusability.
First, define your custom mapper class:
public class DockerNATMapper implements HostAndPortMapper {
// Key: The address reported by Redis (internal).
// Value: The address the client should connect to (external).
private final Map<HostAndPort, HostAndPort> mapping;
public DockerNATMapper(Map<HostAndPort, HostAndPort> mapping) {
this.mapping = mapping;
}
@Override
public HostAndPort getHostAndPort(HostAndPort hostAndPort) {
return mapping.getOrDefault(hostAndPort, hostAndPort);
}
}Then, instantiate this class and pass it to the RedisClusterClient builder:
Map<HostAndPort, HostAndPort> nodeMapping = new HashMap<>();
nodeMapping.put(new HostAndPort("172.18.0.2", 6379), new HostAndPort("my-redis.example.com", 7001));
nodeMapping.put(new HostAndPort("172.18.0.3", 6379), new HostAndPort("my-redis.example.com", 7002));
nodeMapping.put(new HostAndPort("172.18.0.4", 6379), new HostAndPort("my-redis.example.com", 7002));
Set<HostAndPort> initialNodes = new HashSet<>();
// seed node
initialNodes.add(new HostAndPort("my-redis.example.com", 7001));
HostAndPortMapper mapper = new DockerNATMapper(nodeMapping);
JedisClientConfig jedisClientConfig = DefaultJedisClientConfig.builder()
.user("myuser")
.password("mypassword")
.hostAndPortMapper(mapper)
.build();
RedisClusterClient jedisCluster = RedisClusterClient.builder()
.nodes(initialNodes)
.clientConfig(jedisClientConfig)
.build();Now, when RedisClusterClient discovers a node at "172.18.0.2:6379", the mapper will translate it to "localhost:7001" before attempting to connect.
Implementing with a Lambda Expression
Since HostAndPortMapper is a functional interface (it has only one abstract method), you can also provide the implementation more concisely using a lambda expression. This is often preferred for simpler, inline mapping logic.
Map<HostAndPort, HostAndPort> nodeMapping = new HashMap<>();
nodeMapping.put(new HostAndPort("172.18.0.2", 6379), new HostAndPort("my-redis.example.com", 7001));
nodeMapping.put(new HostAndPort("172.18.0.3", 6379), new HostAndPort("my-redis.example.com", 7002));
nodeMapping.put(new HostAndPort("172.18.0.4", 6379), new HostAndPort("my-redis.example.com", 7002));
Set<HostAndPort> initialNodes = new HashSet<>();
initialNodes.add(new HostAndPort("my-redis.example.com", 7001));
HostAndPortMapper mapper = internalAddress -> nodeMapping.getOrDefault(internalAddress, internalAddress);
JedisClientConfig jedisClientConfig = DefaultJedisClientConfig.builder()
.user("myuser")
.password("mypassword")
.hostAndPortMapper(mapper)
.build();
RedisClusterClient jedisCluster = RedisClusterClient.builder()
.nodes(initialNodes)
.clientConfig(jedisClientConfig)
.build();Unix Domain Sockets
Jedis supports connecting to Redis via Unix Domain Sockets (UDS) instead of TCP. This can provide lower latency and higher throughput when the client and server are on the same machine, as it bypasses the TCP/IP stack.
To use UDS, implement the JedisSocketFactory interface with your preferred Unix socket library (e.g., junixsocket) and pass it to RedisClient via a custom ConnectionProvider.
Dependency
Add the junixsocket dependency to your project:
<dependency>
<groupId>com.kohlschutter.junixsocket</groupId>
<artifactId>junixsocket-core</artifactId>
<version>2.10.1</version>
</dependency>Creating a UDS Socket Factory
import org.newsclub.net.unix.AFUNIXSocket;
import org.newsclub.net.unix.AFUNIXSocketAddress;
public class UdsSocketFactory implements JedisSocketFactory {
private final File socketFile;
public UdsSocketFactory(String socketPath) {
this.socketFile = new File(socketPath);
}
@Override
public Socket createSocket() throws JedisConnectionException {
try {
Socket socket = AFUNIXSocket.newStrictInstance();
socket.connect(new AFUNIXSocketAddress(socketFile), Protocol.DEFAULT_TIMEOUT);
return socket;
} catch (IOException e) {
throw new JedisConnectionException("Failed to create UDS connection.", e);
}
}
}Connecting with RedisClient
Create a ConnectionFactory with your UDS socket factory and wrap it in a PooledConnectionProvider:
JedisSocketFactory socketFactory = new UdsSocketFactory("/tmp/redis.sock");
JedisClientConfig clientConfig = DefaultJedisClientConfig.builder().build();
ConnectionFactory connectionFactory = new ConnectionFactory(socketFactory, clientConfig);
PooledConnectionProvider provider = new PooledConnectionProvider(connectionFactory);
RedisClient client = RedisClient.builder()
.connectionProvider(provider)
.clientConfig(clientConfig)
.build();All RedisClient features work the same way over UDS, including connection pooling, RESP3, and client-side caching.
Miscellaneous
A note about String and Binary - what is native?
Redis/Jedis talks a lot about Strings. And here [[http://redis.io/topics/internals]] it says Strings are the basic building block of Redis. However, this stress on strings may be misleading. Redis' "String" refer to the C char type (8 bit), which is incompatible with Java Strings (16-bit). Redis sees only 8-bit blocks of data of predefined length, so normally it doesn't interpret the data (it's "binary safe"). Therefore in Java, byte[] data is "native", whereas Strings have to be encoded before being sent, and decoded after being retrieved by the SafeEncoder. This has some minor performance impact.
In short: if you have binary data, don't encode it into String, but use the binary versions.
A note on Redis' master/slave distribution
A Redis network consists of redis servers, which can be either masters or slaves. Slaves are synchronized to the master (master/slave replication). However, master and slaves look identical to a client, and slaves do accept write requests, but they will not be propagated "up-hill" and could eventually be overwritten by the master. It makes sense to route reads to slaves, and write demands to the master. Furthermore, being a slave doesn't prevent from being considered master by another slave.
Failover (`docs/failover.md`)
Automatic Failover and Failback with Jedis
API was significantly changed in 7.0.0. Please follow the migration guide below.
This feature is experimental and may change in future versions.
Jedis supports failover and failback for your Redis deployments. This is useful when:
- You have more than one Redis deployment. This might include two independent Redis servers or two or more Redis databases replicated across multiple active-active Redis Enterprise clusters.
- You want your application to connect to and use one deployment at a time.
- You want your application to fail over to the next available deployment if the current deployment becomes unavailable.
- You want your application to fail back to the original deployment when it becomes available again.
Jedis will fail over to a subsequent Redis deployment after reaching a configurable failure threshold.
This failure threshold is implemented using a circuit breaker pattern.
You can also configure Jedis to retry failed calls to Redis.
Once a maximum number of retries have been exhausted, the circuit breaker will record a failure.
When the circuit breaker reaches its failure threshold, a failover will be triggered on the subsequent operation.
In the background, Jedis executes configured health checks to determine when a Redis deployment is available again.
When this occurs, Jedis will fail back to the original deployment after a configurable grace period.
The remainder of this guide describes:
- A basic failover and health check configuration
- Supported retry, circuit breaker, failover and failback settings
- Initialization policies that decide when the client is ready (since 7.3.0)
- Failback and the database selection API
- Dynamic weight management for runtime priority adjustments (since 7.4.0)
- Dynamic database management for adding and removing databases at runtime
We recommend that you read this guide carefully and understand the configuration settings before enabling Jedis failover
in production.
Migration from 6.x to 7.x
In Jedis 6.x, failover was supported using special constructor for UnifiedJedis.
In Jedis 7.x, failover is supported using MultiDbClient and MultiDbConfig.builder:
// Jedis 6.x
JedisClientConfig config = DefaultJedisClientConfig.builder().user("cache").password("secret").build();
ClusterConfig[] clientConfigs = new ClusterConfig[2];
clientConfigs[0] = new ClusterConfig(new HostAndPort("redis-east.example.com", 14000), config);
clientConfigs[1] = new ClusterConfig(new HostAndPort("redis-west.example.com", 14000), config);
MultiClusterClientConfig.Builder builder = new MultiClusterClientConfig.Builder(clientConfigs);
// ...
MultiClusterPooledConnectionProvider provider = new MultiClusterPooledConnectionProvider(builder.build());
UnifiedJedis client = new UnifiedJedis(provider);
// Jedis 7.x
// MultiClusterClientConfig was renamed to MultiDbConfig and MultiDbClient with convenient builder was added
MultiDbConfig multiConfig = MultiDbConfig.builder()
.database(DatabaseConfig.builder(east, config).weight(1.0f).build())
.database(DatabaseConfig.builder(west, config).weight(0.5f).build())
.build();
// Use MultiDbClient instead of UnifiedJedis
MultiDbClient multiDbClient = MultiDbClient.builder().multiDbConfig(multiConfig).build();For more details on configuration options see sections below.
Installing optional dependencies
Jedis failover support is provided by optional dependencies.
To use failover, add the following dependencies to your project:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-all</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
<version>1.7.1</version>
</dependency>Basic usage
To configure Jedis for failover, you specify a weighted list of Redis databases.
Jedis will connect to the Redis database in the list with the highest weight.
If the highest-weighted database becomes unavailable,
Jedis will attempt to connect to the database with the next highest weight in the list, and so on.
Database weights determine the priority for selecting which database becomes active. Weights can be configured at initialization and can also be changed dynamically at runtime (introduced in version 7.4.0), allowing you to adjust active database selection priorities without recreating the client.
Suppose you run two Redis deployments.
We'll call them redis-east and redis-west.
You want your application to first connect to redis-east.
If redis-east becomes unavailable, you want your application to connect to redis-west.
Let's look at one way of configuring Jedis for this scenario.
First, start by defining the initial configuration for each Redis database available and prioritize them using weights.
JedisClientConfig config = DefaultJedisClientConfig.builder()
.user("cache").password("secret")
.socketTimeoutMillis(5000).connectionTimeoutMillis(5000).build();
// Custom pool config per database can be provided
ConnectionPoolConfig poolConfig = new ConnectionPoolConfig();
poolConfig.setMaxTotal(8);
poolConfig.setMaxIdle(8);
poolConfig.setMinIdle(0);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setMaxWait(Duration.ofSeconds(1));
poolConfig.setTestWhileIdle(true);
poolConfig.setTimeBetweenEvictionRuns(Duration.ofSeconds(1));
HostAndPort east = new HostAndPort("redis-east.example.com", 14000);
HostAndPort west = new HostAndPort("redis-west.example.com", 14000);
MultiDbConfig.Builder multiConfig = MultiDbConfig.builder()
.database(DatabaseConfig.builder(east, config).connectionPoolConfig(poolConfig).weight(1.0f).build())
.database(DatabaseConfig.builder(west, config).connectionPoolConfig(poolConfig).weight(0.5f).build());The configuration above represents your two Redis deployments: redis-east and redis-west.
If you do not need a custom pool configuration or health check strategy, the shorterdatabase(endpoint, weight, clientConfig) overload builds the DatabaseConfig for you:
MultiDbConfig.Builder multiConfig = MultiDbConfig.builder()
.database(east, 1.0f, config)
.database(west, 0.5f, config);Continue using the MultiDbConfig.Builder builder to set your preferred retry and failover configuration.
Then build a MultiDbClient:
/* Detailed source-code truncated for AI context efficiency. */In the configuration here, we've set a sliding window of 10 seconds, a failure rate threshold of 50% and a minimum of 100 failures.
This means that a failover will be triggered only when, within the last 10 seconds, at least 100 calls to Redis have failed
and the failed calls make up at least 50% of all calls in that window. Both conditions must be met.
You can now use this MultiDbClient instance in your application to execute Redis commands.MultiDbClient also provides failover-aware pipelines and transactions through pipelined() and multi();
see the Transactions page for the MultiDbTransaction specifics.
Configuration options
Under the hood, Jedis' failover support relies on resilience4j,
a fault-tolerance library that implements retry and circuit breakers.
Once you configure a MultiDbClient, each call to Redis is decorated with a resilience4j retry and circuit breaker.
By default, any call that throws a JedisConnectionException will be attempted up to 3 times (the initial call plus 2 retries).
If all attempts fail, the circuit breaker records a failure.
The circuit breaker keeps the outcome of recent calls in a time-based sliding window.
Jedis evaluates two thresholds against that window: a minimum number of failed calls and a failure rate.
When both are reached (e.g., at least 1000 calls failed within the last 2 seconds, and they make up at least 10% of all calls in that window),
the circuit breaker transitions from CLOSED to OPEN.
When this occurs, Jedis will switch to the healthy Redis database with the highest weight in its configuration.
The supported settings, and their default values, are described below.
You can configure any of these settings using the MultiDbConfig.Builder builder.
Refer the basic usage above for an example of this.
Retry configuration
Configuration for command retry behavior is encapsulated in MultiDbConfig.RetryConfig and provided using MultiDbConfig.Builder.commandRetry().
Jedis uses the following retry settings:
| Setting | Builder method | Default value | Description |
|---|---|---|---|
| Max attempts | maxAttempts |
3 | Maximum number of attempts, including the initial call |
| Retry wait duration | waitDuration |
500 ms | Number of milliseconds to wait between retry attempts |
| Wait duration backoff multiplier | exponentialBackoffMultiplier |
2 | Exponential backoff factor multiplied against wait duration between retries. For example, with a wait duration of 1 second and a multiplier of 2, the retries would occur after 1s, 2s, 4s, 8s, 16s, and so on. |
| Retry included exception list | includedExceptionList |
[JedisConnectionException] | A list of Throwable classes that count as failures and should be retried. |
| Retry ignored exception list | ignoreExceptionList |
null | A list of Throwable classes to explicitly ignore for the purposes of retry. |
To disable retry, set maxAttempts to 1.
Circuit breaker configuration
For failover, Jedis uses a circuit breaker to detect when a Redis database has failed.
Failover configuration is encapsulated in MultiDbConfig.CircuitBreakerConfig and can be provided using the MultiDbConfig.Builder.failureDetector().
Jedis uses the following circuit breaker settings:
| Setting | Builder method | Default value | Description |
|---|---|---|---|
| Sliding window size | slidingWindowSize |
2 | Size of the time-based sliding window, in seconds, over which call outcomes are recorded. |
| Threshold min number of failures | minNumOfFailures |
1000 | Minimum number of failed calls within the sliding window before the circuit breaker can trip. 0 disables this check so that only the failure rate is considered. |
| Failure rate threshold | failureRateThreshold |
10.0f |
Percentage of calls within the sliding window that must fail before the circuit breaker transitions to the OPEN state. 0.0f disables this check so that only the minimum number of failures is considered. |
| Circuit breaker included exception list | includedExceptionList |
[JedisConnectionException] | A list of Throwable classes that count as failures and add to the failure rate. |
| Circuit breaker ignored exception list | ignoreExceptionList |
null | A list of Throwable classes to explicitly ignore for failure rate calculations. |
The circuit breaker opens only when both the minimum number of failures and the failure rate threshold are reached within the sliding window.
Once a failover has been triggered, the failed database's circuit breaker is kept open for the configured grace period
(see Failover and failback configuration) so that traffic does not return to it prematurely.
Failover and failback configuration
The following settings on MultiDbConfig.Builder control what happens once a failure has been detected,
and how Jedis returns to a preferred database once it recovers:
| Setting | Builder method | Default value | Description |
|---|---|---|---|
| Failback supported | failbackSupported |
true |
Enables the periodic check that switches back to a healthy database with a higher weight than the active one. |
| Failback check interval | failbackCheckInterval |
120000 ms (2 min) | How often the periodic failback check runs. |
| Grace period | gracePeriod |
60000 ms (1 min) | How long a database stays disabled after it is marked unhealthy or its circuit breaker trips. During this period it is not considered for failover or failback, even if health checks report it healthy. |
| Fast failover | fastFailover |
false |
When enabled, all open connections to the previously active database are forcefully closed on a switch, so in-flight operations fail immediately instead of waiting for their socket timeouts. |
| Retry on failover | retryOnFailover |
false |
When enabled, a command that fails on a database that has just been switched away from is re-executed on the new active database instead of surfacing the failure to the caller. |
| Max number of failover attempts | maxNumFailoverAttempts |
10 | How many times Jedis reports the "no healthy database" condition as temporary before it is treated as permanent. Automatic recovery is not guaranteed after that. See When no database is available. |
| Delay in between failover attempts | delayInBetweenFailoverAttempts |
12000 ms | Minimum time between two counted failover attempts while no healthy database is available. |
| Initialization policy | initializationPolicy |
MAJORITY_AVAILABLE |
Decides when MultiDbClient.builder().build() may return based on the initial health check results. See Initialization policy. |
Initialization policy
Introduced in version 7.3.0
When a MultiDbClient is created, health checks are started for every database that has them enabled,
and the client waits for their first results before selecting the initial active database.
The InitializationPolicy decides how many databases must be reachable before the client is considered ready,
and when initialization should fail instead. The built-in policies live in InitializationPolicy.BuiltIn:
| Policy | Client is ready when... | Initialization fails when... |
|---|---|---|
ALL_AVAILABLE |
every database passed its initial health check | any database fails its initial health check |
MAJORITY_AVAILABLE |
more than half of the databases are healthy | a majority can no longer be reached (default) |
ONE_AVAILABLE |
at least one database is healthy | every database failed its initial health check |
Databases with health checks disabled are counted as available.
If the policy fails, build() throws a JedisConnectionException.
MultiDbConfig config = MultiDbConfig.builder()
.database(east, 1.0f, clientConfig)
.database(west, 0.5f, clientConfig)
.initializationPolicy(InitializationPolicy.BuiltIn.ONE_AVAILABLE) // start as soon as one database is reachable
.build();You can also implement InitializationPolicy yourself. The evaluate method receives the number of available,
failed and pending databases and returns CONTINUE, SUCCESS or FAIL.
When no database is available
If a failover is triggered but no other healthy database exists, the command fails with one of twoJedisConnectionException subclasses from the redis.clients.jedis.mcf package:
JedisFailoverException.JedisTemporarilyNotAvailableExceptionwhile the number of counted failover attempts is
at or belowmaxNumFailoverAttempts. Attempts are counted at most once perdelayInBetweenFailoverAttempts,
so with the defaults the condition is reported as temporary for roughly two minutes.JedisFailoverException.JedisPermanentlyNotAvailableExceptiononce that limit has been exceeded.
The attempt counter resets as soon as a switch to a healthy database succeeds.
While the failure is temporary
Jedis keeps trying and recovers on its own. Applications can catch the temporary variant to back off and retry.
Once the failure is permanent
Jedis gives no guarantee of automatic recovery after JedisPermanentlyNotAvailableException. The client may resume
if a database comes back, but you must not rely on it. User action is required. For example, alert on the exception and
check the databases and the network. Recreating the client (create a new one and close the old one) is the recommended
way to recover.
Health Check Configuration and Customization
The MultiDbClient includes a comprehensive health check system that continuously monitors the availability of Redis databases to enable automatic failover and failback.
The health check system serves several critical purposes in the failover architecture:
- Proactive Monitoring: Continuously monitors all databases, including passive ones that aren't currently receiving traffic
- Failback Detection: Determines when a previously failed database has recovered and is ready to accept traffic
- Circuit Breaker Integration: Works with the circuit breaker pattern to manage database state transitions
- Customizable Strategies: Supports pluggable health check implementations for different deployment scenarios
The health check system operates independently of your application traffic, running background checks at configurable intervals to assess database health without impacting performance.
How a health check run works
Every HealthCheckStrategy exposes a small set of parameters that drive the background check:
| Parameter | Accessor | HealthCheckStrategy.Config default |
Description |
|---|---|---|---|
| Interval | getInterval() |
5000 ms | Time between two health check runs |
| Timeout | getTimeout() |
1000 ms | Maximum time a single probe may take; a timed out probe counts as a failure |
| Number of probes | getNumProbes() |
3 | How many times doHealthCheck is invoked (at most) within one run |
| Delay in between probes | getDelayInBetweenProbes() |
500 ms | Pause between two probes of the same run |
| Probing policy | getPolicy() |
ProbingPolicy.BuiltIn.ALL_SUCCESS |
Decides how the individual probe results are combined into one HEALTHY / UNHEALTHY result |
The built-in probing policies in ProbingPolicy.BuiltIn are:
ALL_SUCCESS— every probe must succeed; the run stops at the first failed probe.ANY_SUCCESS— one successful probe is enough; the run stops at the first successful probe.MAJORITY_SUCCESS— more than half of the probes must succeed; the run stops as soon as the outcome is decided.
The result of a run becomes the database's health status. When the active database turns unhealthy, Jedis puts it into
the grace period and switches to the healthy database with the highest weight.
Available Health Check Types
1. PingStrategy (Default)
The PingStrategy is the default health check implementation. It keeps a small dedicated connection pool to the database
and sends the Redis PING command to verify connectivity and that the server is responding.
Use Cases:
- General-purpose health checking for most Redis deployments
- Simple connectivity validation
How it works:
- Sends
PINGcommand to the Redis server using the database'sJedisClientConfig(so the same credentials, TLS settings, and timeouts apply) - Expects exact response
"PONG"to consider the probe successful - Any exception, timeout, or unexpected response marks the probe as failed
- Probe results are combined according to the
HealthCheckStrategy.Configshown above (defaults: 3 probes, all must succeed)
PingStrategy.DEFAULT is used when no strategy supplier is configured. To tune its interval, timeout, or probing behavior,
pass a HealthCheckStrategy.Config through your own supplier:
HealthCheckStrategy.Config pingConfig = HealthCheckStrategy.Config.builder()
.interval(2000) // Check every 2 seconds
.timeout(500) // 500ms timeout per probe
.numProbes(2)
.policy(ProbingPolicy.BuiltIn.ANY_SUCCESS)
.build();
MultiDbConfig.StrategySupplier pingSupplier =
(hostAndPort, jedisClientConfig) -> new PingStrategy(hostAndPort, jedisClientConfig, pingConfig);
MultiDbConfig.DatabaseConfig dbConfig =
MultiDbConfig.DatabaseConfig.builder(east, config)
.healthCheckStrategySupplier(pingSupplier)
.build();2. LagAwareStrategy [PREVIEW] (Redis Enterprise)
The LagAwareStrategy is designed specifically for Redis Enterprise Active-Active deployments and uses the Redis Enterprise REST API to check database availability and replication lag.
Use Cases:
- Redis Enterprise Active-Active (CRDB) deployments
- Scenarios where replication lag tolerance is critical
- Enterprise environments with REST API access
How it works:
- Queries Redis Enterprise REST API for database availability
- Optionally validates replication lag against a configurable tolerance (
extendedCheckEnabled, defaulttrue;availabilityLagTolerance, default 5 seconds) - Automatically discovers the database ID by matching the database endpoint's hostname against the databases exposed by the REST API
Example Configuration:
// REST API endpoint and credentials of the Redis Enterprise cluster
HostAndPort restEndpoint = new HostAndPort("redis-enterprise-cluster-fqdn", 9443);
Supplier<RedisCredentials> restCredentials = () ->
new DefaultRedisCredentials("rest-api-user", "pwd");
LagAwareStrategy.Config lagConfig = LagAwareStrategy.Config.builder(restEndpoint, restCredentials)
.interval(5000) // Check every 5 seconds
.timeout(3000) // 3 second timeout per REST call
.extendedCheckEnabled(true) // Also validate replication lag
.availabilityLagTolerance(Duration.ofSeconds(2)) // Tolerate up to 2 seconds of lag
// .sslOptions(sslOptions) // Optional: custom truststore for the HTTPS REST API
.build();
MultiDbConfig.StrategySupplier lagAwareSupplier =
(hostAndPort, jedisClientConfig) -> new LagAwareStrategy(lagConfig);
MultiDbConfig.DatabaseConfig dbConfig =
MultiDbConfig.DatabaseConfig.builder(east, config)
.healthCheckStrategySupplier(lagAwareSupplier)
.build();LagAwareStrategy.Config also offers the shortcuts databaseAvailability(...) (availability only, no lag check),lagAware(...) (default tolerance) and lagAwareWithTolerance(...) for the common configurations.
3. Custom Health Check Strategies
You can implement custom health check strategies by implementing the HealthCheckStrategy interface.
Use Cases:
- Application-specific health validation logic
- Integration with external monitoring systems
- Custom performance or latency-based health checks
Use the healthCheckStrategySupplier() method to provide a custom health check implementation.
The supplier receives the database's HostAndPort and JedisClientConfig, so one supplier can serve several databases:
// Custom strategy supplier
MultiDbConfig.StrategySupplier customStrategy =
(hostAndPort, jedisClientConfig) -> {
// Return your custom HealthCheckStrategy implementation
return new MyCustomHealthCheckStrategy(hostAndPort, jedisClientConfig);
};
MultiDbConfig.DatabaseConfig dbConfig =
MultiDbConfig.DatabaseConfig.builder(east, config)
.healthCheckStrategySupplier(customStrategy)
.weight(1.0f)
.build();If you already have a configured strategy instance, healthCheckStrategy(HealthCheckStrategy) wraps it in a supplier for you.
The same instance is then reused for every database that shares that DatabaseConfig, so make sure it is thread-safe.
A minimal HealthCheckStrategy looks like this. Note that doHealthCheck receives the Endpoint being checked,
and that close() is called when the database is removed or the client is closed, so resources should be created once
and released there rather than per probe:
public class MyCustomHealthCheckStrategy implements HealthCheckStrategy {
private final RedisClient client;
public MyCustomHealthCheckStrategy(HostAndPort hostAndPort, JedisClientConfig jedisClientConfig) {
this.client = RedisClient.builder().hostAndPort(hostAndPort).clientConfig(jedisClientConfig).build();
}
@Override
public int getInterval() {
return 1000; // Check every second
}
@Override
public int getTimeout() {
return 500; // 500ms timeout per probe
}
@Override
public int getNumProbes() {
return 1;
}
@Override
public ProbingPolicy getPolicy() {
return ProbingPolicy.BuiltIn.ANY_SUCCESS;
}
@Override
public int getDelayInBetweenProbes() {
return 100;
}
@Override
public HealthStatus doHealthCheck(Endpoint endpoint) {
try {
String result = client.ping();
return "PONG".equals(result) ? HealthStatus.HEALTHY : HealthStatus.UNHEALTHY;
} catch (Exception e) {
return HealthStatus.UNHEALTHY;
}
}
@Override
public void close() {
client.close();
}
}Disabling Health Checks
Use the healthCheckEnabled(false) method to completely disable health checks for a database:
MultiDbConfig.DatabaseConfig dbConfig = MultiDbConfig.DatabaseConfig.builder(east, config)
.healthCheckEnabled(false) // Disable health checks entirely
.build();A database without health checks is always assumed healthy, so it is only taken out of rotation by its circuit breaker
(for the duration of the grace period). Failures are then detected exclusively through the commands your application executes,
and the database is considered available again as soon as the grace period ends.
Fallback configuration
Fallback is what makes a command that hits an open circuit breaker, or that fails while a failover is in progress,
switch to the next database and run there instead of surfacing the error.
Jedis uses the following fallback settings:
| Setting | Builder method | Default value | Description |
|---|---|---|---|
| Fallback exception list | fallbackExceptionList |
[CallNotPermittedException, ConnectionFailoverException] | A list of Throwable classes that trigger fallback. |
CallNotPermittedException is thrown by resilience4j when the circuit breaker is open.ConnectionFailoverException wraps a command failure that happened on a database which is no longer active,
and is only produced when retryOnFailover is enabled.
Failover callbacks
In the event that Jedis fails over, you may wish to take some action. This might include logging a warning, recording
a metric, or externally persisting the database connection state, to name just a few examples. For this reason,MultiDbClient lets you register a custom callback that will be called whenever Jedis
switches to a new database.
To use this feature, you'll need to design a class that implements java.util.function.Consumer<DatabaseSwitchEvent>.
This class must implement the accept method, as you can see below.
public class FailoverReporter implements Consumer<DatabaseSwitchEvent> {
@Override
public void accept(DatabaseSwitchEvent e) {
System.out.println("Jedis switched to database: " + e.getDatabaseName() + " due to " + e.getReason());
}
}The DatabaseSwitchEvent consumer is registered on the client builder, next to the MultiDbConfig:
FailoverReporter reporter = new FailoverReporter();
MultiDbClient client = MultiDbClient.builder()
.multiDbConfig(multiConfig)
.databaseSwitchListener(reporter)
.build();The client will call your accept whenever the active database changes.
You can also use a lambda expression:
MultiDbClient client = MultiDbClient.builder()
.multiDbConfig(multiConfig)
.databaseSwitchListener(event -> System.out.println("Switched to: " + event.getEndpoint()))
.build();DatabaseSwitchEvent.getReason() returns a SwitchReason telling you why the switch happened:
SwitchReason |
Triggered by |
|---|---|
CIRCUIT_BREAKER |
The active database's circuit breaker tripped |
HEALTH_CHECK |
The active database is unhealthy |
FAILBACK |
The periodic failback check found a healthy database with a higher weight |
FORCED |
setActiveDatabase(), forceActiveDatabase(), or removal of the active database via removeDatabase() |
Failing back
Jedis supports automatic failback based on health checks or manual failback using the database selection API.
Failback scenario
When a failover is triggered, Jedis will attempt to connect to the next Redis server based on the weights of server configurations
you provide at setup.
For example, recall the redis-east and redis-west deployments from the basic usage example above.
Jedis will attempt to connect to redis-east first.
If redis-east becomes unavailable (and the circuit breaker transitions), then Jedis will attempt to use redis-west.
Now suppose that redis-east eventually comes back online.
You will likely want to fail your application back to redis-east.
Automatic failback based on health checks
When health checks are enabled, Jedis automatically monitors the health of all configured databases, including those that are currently inactive due to previous failures.
The automatic failback process works as follows:
- Continuous Monitoring: Health checks run continuously for all databases, regardless of their current active status
- Recovery Detection: When a health check run on a previously failed database succeeds (according to the strategy's probing policy), the database is marked as healthy
- Grace Period Respect: A database is not eligible for failback until the configured grace period (
gracePeriod, default 1 minute) has elapsed since it was marked unhealthy - Weight-Based Failback: Every
failbackCheckInterval(default 2 minutes), iffailbackSupportedis enabled and a healthy, eligible database has a higher weight than the currently active database, Jedis switches to it and emits aFAILBACKswitch event
With the default settings, failback can therefore take up to a few minutes after the database recovers.
Lower failbackCheckInterval and gracePeriod if you need a faster failback, keeping in mind that a short grace period
increases the risk of oscillating between databases during intermittent failures.
Manual Failback using the database selection API
Once you've determined that it's safe to fail back to a previously-unavailable database,
you need to decide how to trigger the failback. There are two ways to accomplish this:
setActiveDatabase(endpoint) switches to the given database right away.
Jedis first validates the target by opening a connection and sending PING; if that fails, a JedisValidationException
is thrown and the active database is left unchanged. On success a FORCED switch event is emitted.
Endpoint endpoint = new HostAndPort("redis-east.example.com", 14000);
client.setActiveDatabase(endpoint);Note that automatic failback keeps running: if a healthy database with a higher weight exists, the next periodic failback
check may switch away from the database you selected. Use forceActiveDatabase if you want the choice to stick.
forceActiveDatabase(endpoint, forcedActiveDurationMs) switches to the given database and puts every other database
into a grace period for the given duration, so neither failover nor failback will move traffic away from it during that time.
Because the other databases are in their grace period, commands fail with the exceptions described in
When no database is available if the pinned database itself goes down.
The target must be healthy, otherwise a JedisValidationException is thrown.
// Pin redis-east as the active database for the next 10 minutes
client.forceActiveDatabase(endpoint, Duration.ofMinutes(10).toMillis());Both methods are thread-safe.
If you decide to implement manual failback, you will need a way for external systems to trigger this method in your
application. For example, if your application exposes a REST API, you might consider creating a REST endpoint
to call setActiveDatabase and fail back the application.
Dynamic Weight Management
Introduced in version 7.4.0
Jedis allows you to dynamically adjust database weights at runtime without recreating the MultiDbClient.
Important: Weight determines the priority for selecting which database becomes the active database. At any given time, only ONE database is active and receives all traffic. Weight does not distribute load across databases - it determines which single database Jedis will prefer to use as the active connection.
This is useful for scenarios where you need to change the active database selection priority based on operational conditions, such as:
- Changing which database should be preferred during planned maintenance
- Adjusting selection priority based on database performance or regional preferences
- Implementing controlled switchover between databases
- Responding to changing infrastructure conditions
Getting and Setting Weights
The MultiDbClient provides methods to query and modify database weights at runtime:
// Get the current weight of a database
HostAndPort east = new HostAndPort("redis-east.example.com", 14000);
float currentWeight = client.getWeight(east);
System.out.println("Current weight: " + currentWeight);
// Set a new weight for a database
client.setWeight(east, 2.0f);Weight Constraints
When setting weights dynamically, the following constraints apply:
- Weight must be greater than 0: Attempting to set a weight of 0 or negative values will throw an
IllegalArgumentException - Endpoint must exist: The endpoint must be part of the configured databases, otherwise a
JedisValidationExceptionis thrown
Runtime Behavior
When you change a database's weight at runtime:
- Immediate Effect on Selection: The weight change takes effect immediately for future active database selection decisions during failover or failback
- Automatic Failback Trigger: If automatic failback is enabled and you increase a database's weight above the currently active database, Jedis will automatically switch to the higher-weight database during the next periodic failback check (if the database is healthy and the grace period has elapsed)
- No Disruption: Changing weights does not interrupt ongoing operations or force an immediate switch
- Single Active Database: Remember that only one database is active at any time - all traffic goes to that single database
How Weight Affects Database Selection
During failover or failback, Jedis selects the active database by:
- Filtering for healthy databases (passing health checks, not in grace period, circuit breaker not open)
- Sorting the healthy databases by weight in descending order (highest weight first)
- Selecting the first database from this sorted list as the active database
Example: Changing Active Database Priority
Here's a practical example of dynamically adjusting weights to control which database should be active:
// Initial configuration - primary has higher weight, so it will be selected as active
HostAndPort primary = new HostAndPort("redis-primary.example.com", 6379);
HostAndPort secondary = new HostAndPort("redis-secondary.example.com", 6379);
MultiDbConfig config = MultiDbConfig.builder()
.database(DatabaseConfig.builder(primary, clientConfig).weight(2.0f).build())
.database(DatabaseConfig.builder(secondary, clientConfig).weight(1.0f).build())
.failbackSupported(true)
.failbackCheckInterval(1000)
.build();
MultiDbClient client = MultiDbClient.builder()
.multiDbConfig(config)
.build();
// At this point, 'primary' is the active database (weight 2.0 > 1.0)
// Before planned maintenance on primary, make secondary the preferred database
client.setWeight(secondary, 3.0f); // Now secondary has highest weight
// During the next failback check, Jedis will switch to secondary as the active database
// After maintenance, restore primary as the preferred database
client.setWeight(primary, 4.0f); // Now primary has highest weight again
// During the next failback check, Jedis will switch back to primary as the active databaseMonitoring Database Switches
You can combine weight changes with failover callbacks to monitor when the active database switches due to weight adjustments:
MultiDbClient client = MultiDbClient.builder()
.multiDbConfig(config)
.databaseSwitchListener(event -> {
System.out.println("Active database switched to: " + event.getEndpoint() +
" due to: " + event.getReason());
})
.build();
// Change weight - may trigger automatic failback to switch active database
client.setWeight(secondary, 5.0f);Dynamic Database Management
Jedis allows you to dynamically add and remove database endpoints at runtime without recreating the MultiDbClient. This provides flexibility for scenarios such as:
- Adding new database replicas or regions as they become available
- Removing databases during planned maintenance or decommissioning
- Scaling your Redis infrastructure dynamically
- Responding to infrastructure changes without application restarts
Adding Databases at Runtime
The MultiDbClient provides two overloaded methods for adding databases dynamically:
Method 1: Using DatabaseConfig
This method provides maximum flexibility for advanced configurations including custom health check strategies, connection pool settings, and other database-specific options.
// Create a fully configured DatabaseConfig
HostAndPort newEndpoint = new HostAndPort("redis-new.example.com", 6379);
JedisClientConfig clientConfig = DefaultJedisClientConfig.builder()
.user("cache").password("secret").build();
DatabaseConfig databaseConfig = DatabaseConfig.builder(newEndpoint, clientConfig)
.weight(1.5f).connectionPoolConfig(poolConfig).healthCheckEnabled(true).build();
// Add the database to the client
client.addDatabase(databaseConfig);Method 2: Using Endpoint, Weight, and ClientConfig
This is a convenience method for simpler configurations when you don't need advanced customization.
HostAndPort newEndpoint = new HostAndPort("redis-new.example.com", 6379);
JedisClientConfig clientConfig = DefaultJedisClientConfig.builder()
.user("cache").password("secret").build();
// Add the database with basic configuration
client.addDatabase(newEndpoint, 1.5f, clientConfig);Removing Databases at Runtime
You can remove database endpoints dynamically using the removeDatabase() method:
HostAndPort endpointToRemove = new HostAndPort("redis-old.example.com", 6379);
// Remove the database from the client
client.removeDatabase(endpointToRemove);Behavior and Constraints
When adding or removing databases, the following behavior applies:
Adding Databases
- Immediate Availability: The new endpoint becomes available for failover operations immediately after being added and, if health checks are enabled, after its first health check run succeeds
- Health Check Integration: If health checks are configured, the new database will be monitored according to the configured health check strategy
- Duplicate Prevention: Attempting to add an endpoint that already exists will throw a
JedisValidationException - Weight-Based Selection: The new database participates in weight-based active database selection according to its configured weight. Adding a database does not switch the active database by itself; if the new database has the highest weight, the next periodic failback check will switch to it
Removing Databases
- Automatic Failover: If the removed endpoint is currently the active database, Jedis will automatically switch to the healthy endpoint with the highest weight and emit a
FORCEDswitch event. If no other healthy endpoint exists, the removal is rejected with aJedisException - Last Database Protection: You cannot remove the last remaining endpoint - attempting to do so will throw a
JedisValidationException - Non-Existent Endpoint: Attempting to remove an endpoint that doesn't exist will throw a
JedisValidationException - Resource Cleanup: The removed database's connections and resources are properly closed and cleaned up
- Health Check Cleanup: Health checks for the removed database are automatically stopped and unregistered
Querying Configured Databases
You can retrieve the set of all currently configured database endpoints, inspect their health, and find out which one is active:
Set<Endpoint> endpoints = client.getDatabaseEndpoints();
System.out.println("Configured databases: " + endpoints);
Endpoint active = client.getActiveDatabaseEndpoint();
System.out.println("Active database: " + active);
for (Endpoint endpoint : endpoints) {
System.out.println(endpoint + " healthy: " + client.isHealthy(endpoint));
}isHealthy(endpoint) reflects the same view Jedis uses for failover decisions: the database's health check status,
whether its circuit breaker is open, and whether it is currently in a grace period.
Complete Example: Dynamic Database Management
Here's a practical example demonstrating dynamic database management:
/* Detailed source-code truncated for AI context efficiency. */Thread Safety
Both addDatabase() and removeDatabase() methods are thread-safe and can be called concurrently from multiple threads. The client ensures that database additions and removals are properly synchronized with ongoing operations.
Troubleshooting Failover and Failback Issues
Health Checks Always Report Unhealthy
Common causes:
- Timeout too aggressive for network conditions
- Authentication issues with Redis server (the health check uses the database's
JedisClientConfig) - Network connectivity problems
Solutions:
// Increase timeout values
HealthCheckStrategy.Config hcConfig = HealthCheckStrategy.Config.builder()
.timeout(3000) // Increase from default 1000ms
.build();
DatabaseConfig dbConfig = DatabaseConfig.builder(east, config)
.healthCheckStrategySupplier((hostAndPort, clientConfig) -> new PingStrategy(hostAndPort, clientConfig, hcConfig))
.build();Intermittent Health Check Failures
Solutions:
// Tolerate a single failed probe instead of requiring all probes to succeed
HealthCheckStrategy.Config hcConfig = HealthCheckStrategy.Config.builder()
.interval(5000) // Less frequent checks
.timeout(2000) // More generous timeout
.numProbes(3)
.policy(ProbingPolicy.BuiltIn.MAJORITY_SUCCESS) // 2 out of 3 probes must succeed
.build();Slow Failback After Recovery
With the defaults, a recovered database is picked up again only after its 1 minute grace period has passed
and the next failback check runs, which happens every 2 minutes.
Solutions:
// Faster recovery detection
HealthCheckStrategy.Config hcConfig = HealthCheckStrategy.Config.builder()
.interval(1000) // More frequent checks
.build();
// Adjust failback timing
MultiDbConfig multiConfig = MultiDbConfig.builder()
.database(east, 1.0f, config)
.database(west, 0.5f, config)
.failbackCheckInterval(5000) // Look for a better database every 5 seconds
.gracePeriod(10000) // Shorter grace period
.build();JedisPermanentlyNotAvailableException
Cause: no database was usable for longer than the configured tolerance (maxNumFailoverAttempts counted attempts,
at most one every delayInBetweenFailoverAttempts).
Solutions:
- Jedis gives no guarantee of automatic recovery after this exception. Recreating the client (create a new one and close
the old one) is the recommended way to recover. Checking the databases and the network is a good first step. - To ride out longer outages without recreating the client, increase
maxNumFailoverAttemptsanddelayInBetweenFailoverAttempts. See When no database is available.
Need help or have questions?
For assistance with this automatic failover and failback feature,
start a discussion.
Faq (`docs/faq.md`)
Frequently Asked Questions
If you get `java.net.SocketTimeoutException: Read timed out` exception
Try setting own timeout value when constructing JedisPool using the following constructor:
JedisPool(GenericObjectPoolConfig poolConfig, String host, int port, int timeout)where timeout is given as milliseconds.
Default timeout value is 2 seconds.
JedisPool blocks after getting 8 connections
JedisPool defaults to 8 connections, you can change this in the PoolConfig:
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(maxTotal); // maximum active connections
poolConfig.setMaxIdle(maxIdle); // maximum idle connectionsTake into account that JedisPool inherits commons-pool BaseObjectPoolConfig which has a lot of configuration parameters.
We've set some defined ones which suit most of the cases. In case, you experience issues tuning these parameters may help.
How to configure the buffer size of socket(s)
The buffer size of all Jedis sockets in an application can be configured through system property.
Buffer size of input stream can be configured by setting jedis.bufferSize.input or jedis.bufferSize system property.
Buffer size of output stream can be configured by setting jedis.bufferSize.output or jedis.bufferSize system property.
If you want to set the buffer size of both input and output stream to same value, you can just set jedis.bufferSize.
Note: This feature is available since Jedis 4.2.0.
How to avoid cluster initialization error
As of Jedis 4.0.0, a JedisClusterOperationException is raised with the message Could not initialize cluster slots cache. when the cluster initialization process fails.
Should you would want to avoid this error (for example, creating JedisConnectionFactory to an unavailable cluster for a spring-data-redis Bean), set the system property jedis.cluster.initNoError to any value.
In the console, add the option -Djedis.cluster.initNoError.
In an application, System.setProperty("jedis.cluster.initNoError", ""); can be set before creating any cluster object.
Note: This feature is available since Jedis 4.4.2.
Hash Import (`docs/hash-import.md`)
Hash Import (HIMPORT)
Overview
The HIMPORT command (Redis 8.10 and later) is a fast way to create many hashes
that share a common set of field names — for example, importing a batch of records where every
record has the same columns. Rather than sending the field names again for every hash (as a
series of HSET calls would), the field names are declared once per connection and each hash
then supplies only its values, positionally matched to those fields.
On the wire this is a connection-local container command: HIMPORT PREPARE registers a named
field-set (the ordered field names) on the current connection, HIMPORT SET creates one hash
from a row of values against that field-set, and HIMPORT DISCARD releases it. A field-set
lives only on the connection that prepared it and disappears when that connection is reset or
closed.
Depending on the client, commands travel over a single direct connection (Jedis) or over a
pool of connections (RedisClient, RedisClusterClient) — with a pool, each himportSet can
end up on an arbitrary connection. Rather than expose the raw connection-local commands — which
are not safe to drive by hand across pooled connections — Jedis exposes a reusable HashImport
field-set template plus a per-row himportSet command, and manages the lifecycle: theHIMPORT PREPARE is injected automatically the first time a template is seen on each
connection, a redirect to another cluster node simply re-prepares on demand, and closing the
template schedules HIMPORT DISCARD on every connection it touched. You never managePREPARE/SET/DISCARD ordering or connection pinning yourself.
HashImportandhimportSetare marked@Experimental: the API may change in a future
release. They require Redis 8.10 or later.
Usage
Create a field-set template once (declaring the shared field names, in order), then import each
hash by supplying its key and its values positionally against those fields:
import redis.clients.jedis.HashImport;
import redis.clients.jedis.RedisClient;
RedisClient client = RedisClient.create("redis://localhost:6379");
// declare the field names shared by every hash we are importing; reusable and safe to share
try (HashImport fields = HashImport.of("name", "email", "age")) {
// import as many hashes as you like — one command per hash, so the total is unbounded
client.himportSet("user:1", fields, "alice", "[email protected]", "30");
client.himportSet("user:2", fields, "bob", "[email protected]", "25");
client.himportSet("user:3", fields, "carol", "[email protected]", "42");
}After these complete, user:1, user:2 and user:3 each exist as an ordinary hash with thename, email and age fields set to their respective values — readable with HGETALL,HGET and every other hash command. Any existing hash at an imported key is replaced.
Closing the template (try-with-resources or close()) schedules a best-effortHIMPORT DISCARD to release the server-side state; it is optional (the state also dies with
the connection) but good hygiene for long-lived connections. A closed template must not be used
again.
Pipelining
For large imports, combine the template with a pipeline to avoid a round trip per hash. TheHIMPORT PREPARE is buffered automatically ahead of the first use on the pipeline's
connection, and its reply is internal — syncAndReturnAll() returns exactly one result per
command you issued:
import redis.clients.jedis.HashImport;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.RedisClient;
import redis.clients.jedis.Response;
RedisClient client = RedisClient.create("redis://localhost:6379");
try (HashImport fields = HashImport.of("name", "email", "age");
Pipeline pipeline = client.pipelined()) {
Response<String> u1 = pipeline.himportSet("user:1", fields, "alice", "[email protected]", "30");
Response<String> u2 = pipeline.himportSet("user:2", fields, "bob", "[email protected]", "25");
Response<String> u3 = pipeline.himportSet("user:3", fields, "carol", "[email protected]", "42");
pipeline.sync();
// each returns "OK"
u1.get();
u2.get();
u3.get();
}Cluster
himportSet works unchanged with RedisClusterClient: each key routes to its slot, and the
template re-prepares per node as needed.
Limitations
- Transactions:
himportSetis not supported insideMULTI/EXEC— the connection-localPREPAREcannot be staged in a transaction. Attempting it throwsUnsupportedOperationException. - Cluster pipelines: not supported. Use the standard (non-pipelined)
himportSetonRedisClusterClientinstead. - A template dropped without
close()leaves its server-side state on the connections it
touched until those connections are recycled.
Integration Testing (`docs/integration-testing.md`)
Integration Testing Infrastructure
This document describes how Jedis integration tests are wired up: the Redis test
environment, how it is started, how tests discover their servers, and how this
maps to CI.
Tips & tricks
Tests do not start Redis. They connect to servers that are already running. The
servers are defined by src/test/resources/env/docker-compose.yml, started viamake start (Makefile), and described to the tests bysrc/test/resources/endpoints.json. A test selects a server by name throughEndpoints.getRedisEndpoint("<name>")
(src/test/java/redis/clients/jedis/Endpoints.java).
- Run integration tests locally:
make start version=8.6 && mvn clean verify && make stop. - Run one test fast:
make start version=8.6thenmvn -Dtest=YourIT verify
(don't forgetmake stop). - Add a new server: add a service to
src/test/resources/env/docker-compose.yml
(setREDIS_CLUSTER/TLS_ENABLED/TLS_CLIENT_CNS/ports/volumes), then add a
matching named entry tosrc/test/resources/endpoints.json. - Reference it in a test:
Endpoints.getRedisEndpoint("<name>")
(src/test/java/redis/clients/jedis/Endpoints.java) →endpoint.getClientConfigBuilder().build(). - TLS in a test: build a truststore from
endpoint.getCertificatesLocation()
viaTlsUtil(src/test/java/redis/clients/jedis/util/TlsUtil.java); certs
come from<TEST_WORK_FOLDER>/<env>/work/tls/. - A test got skipped, not run: its endpoint name is missing from the active
endpoints file (src/test/resources/endpoints.jsonfoross-docker,src/test/resources/endpoints_source.jsonfoross-source) — you're probably
on theoss-source(local) provider. - Reproducing CI: the test build pins Java 8.
1. The test environment is provided by a shared container
All Redis servers used in tests run inside the redislabs/client-libs-test
Docker image, published publicly on
Docker Hub. The same image
is used across several Redis client libraries (Jedis, Lettuce, redis-py,
node-redis, go-redis, …).
The image wraps a chosen Redis version and bootstraps a standalone,
replication + sentinel, or cluster topology — optionally with
TLS / mTLS and modules — entirely from environment variables. Tags follow the
Redis version (e.g. redislabs/client-libs-test:8.6). It is not for
production use.
You normally never run it by hand — make start brings up the whole topology via
Docker Compose (see §4). But the image is self-contained, so you can launch a
single instance directly to experiment:
# standalone on :6379
docker run --rm -p 6379:6379 redislabs/client-libs-test:8.6
# 3-node cluster on :7000-7002
docker run --rm -p 7000-7002:7000-7002 \
-e REDIS_CLUSTER=yes -e NODES=3 -e PORT=7000 redislabs/client-libs-test:8.61.1 Container behaviour
| Concern | How it works |
|---|---|
| Base image | redis:<tag> (OSS) or redis/redis-stack:<tag> (with modules), selectable at build time via BASE_IMAGE / BASE_IMAGE_TAG. |
| Topology | REDIS_CLUSTER=yes → cluster (auto-bootstrapped with redis-cli --cluster create); otherwise standalone/replicated. NODES=N sets node count (cluster default 3, standalone 1). REPLICAS=N sets replicas per cluster master. |
| Ports | Non-TLS nodes: PORT, PORT+1, … (default PORT=3000). TLS nodes: TLS_PORT, TLS_PORT+1, … (default TLS_PORT=4430). |
| Auth | REDIS_PASSWORD → requirepass+masterauth. REDIS_CLIENT_USER/REDIS_CLIENT_PASSWORD are the credentials the container itself uses to create/check the cluster. |
| Protected mode | PROTECTED_MODE=yes sets Redis protected-mode (default no). |
| TLS | TLS_ENABLED=yes auto-generates a self-signed CA, a server cert, and client certs into /redis/work/tls/, and enables a TLS port per node. |
| mTLS | TLS_CLIENT_CNS="cn1 cn2 …" generates one client cert (+ .p12) per CN. TLS_AUTH_CLIENTS_USER=CN maps the cert CN to a Redis ACL user (Redis ≥ 8.6); off disables client-cert auth. |
| Modules | When the base image ships modules (e.g. redis/redis-stack:<tag>), they are auto-loaded. redislabs/client-libs-test has matching …-stack style tags for module testing. |
| Extra directives | Any trailing args become redis-server flags (e.g. --maxmemory 256mb). A few are container-managed and cannot be overridden: --port, --dir, --logfile, --pidfile, --cluster-enabled, --cluster-config-file, --protected-mode. |
1.2 Volumes & directory conventions
| Mount | Purpose |
|---|---|
-v <host>/config:/redis/config:r |
Input. Pre-baked per-node config. Node dirs must be named node-[-<tlsport>]; a sentinel node dir must be named node-sentinel-<...>. Each may contain redis.conf and (for cluster) nodes.conf. Optional tls/ subdir supplies pre-generated certificates. |
-v <host>/work:/redis/work:rw |
Output. Runtime state: node-/{redis.conf,redis.log,redis.pid,nodes.conf} and generated tls/ (ca.crt, ca.key, redis.crt, redis.key, <cn>.p12, …). Tests read TLS certs from here. |
1.3 Generated TLS material
When TLS_ENABLED=yes, the container writes these files to its/redis/work/tls/ directory (surfaced on the host under the work/ mount — see
§1.2 — which is where the tests read them from):
| File | What it is |
|---|---|
ca.crt, ca.key |
Self-signed test CA (signs everything else). |
redis.crt, redis.key |
Server certificate (CN localhost). |
<cn>.crt, <cn>.key, <cn>.p12 |
One client cert per name in TLS_CLIENT_CNS, for mTLS. |
The PKCS#12 keystores (*.p12) use the password changeit. Certs are
generated once and reused on container restart.
1.4 "Endpoint" definition
An endpoint is a host:port pair for one Redis node (127.0.0.1:6379
non-TLS, 127.0.0.1:6390 TLS). For cluster, every node is an endpoint and the
client discovers the rest via CLUSTER NODES.
2. How Jedis defines its environment
Everything lives under src/test/resources/env/.
src/test/resources/env/
├── docker-compose.yml # all Redis services (standalone/sentinel/cluster/tls/mtls/stack)
├── .env # base vars (default REDIS_VERSION, image name, work dir)
├── .env.v6.2 … .env.v8.12 # per-version overrides (just pin REDIS_VERSION)
├── redis1-2-5-8-sentinel/ # one dir per environment
│ ├── config/node-<port>/redis.conf # mounted read-only into the container
│ └── work/ # runtime state (TLS certs land in work/tls/)
├── standalone2-sentinel/
├── cluster-unbound/
├── ...Key .env values:
REDIS_VERSION=8.6.1
CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test
REDIS_ENV_CONF_DIR=./ # base for the :/redis/config mounts
REDIS_ENV_WORK_DIR=/tmp/redis-env-work # base for the :/redis/work mountsEach docker-compose.yml service is one client-libs-test container, e.g.:
redis1-2-5-8-sentinel:
<<: *client-libs-image # image: ${CLIENT_LIBS_TEST_IMAGE}:${REDIS_VERSION}
environment: [REDIS_CLUSTER=no, REDIS_CLIENT_USER=deploy,
REDIS_CLIENT_PASSWORD=verify, TLS_ENABLED=yes]
ports: ["6379:6379", "6390:6390", "26379:26379", "36379:36379"]
volumes:
- ${REDIS_ENV_CONF_DIR}/redis1-2-5-8-sentinel/config:/redis/config:r
- ${REDIS_ENV_WORK_DIR}/redis1-2-5-8-sentinel/work:/redis/work:rwThe compose file provides the full matrix: standalone (+TLS/+ACL), sentinel,cluster-unbound, cluster-stable (+TLS), standalone-mtls, cluster-mtls,
a Redis-Stack service, Toxiproxy (network fault injection), and an explicitly
unavailable instance for failover tests.
3. How tests discover servers: `endpoints.json`
Tests never hardcode hosts. They look up a named entry insrc/test/resources/endpoints.json, loaded by Endpoints.getRedisEndpoint(name).
3.1 Schema
{
"standalone0-tls": {
"endpoints": ["rediss://localhost:6390"], // one URI (standalone) or many (cluster)
"tls": true, // required
"username": "default", // optional ACL user
"password": "foobared", // optional
"tls_cert_path": "redis1-2-5-8-sentinel/work/tls", // optional; resolved under TEST_WORK_FOLDER
"bdbId": 0 // optional (Redis Enterprise only)
}
}redis:// = plaintext, rediss:// = TLS. Names encode the topology and mode,
e.g. standalone0, standalone0-acl, standalone0-tls,standalone0-tls-wronghost (CN-mismatch negative test), cluster-stable,cluster-stable-tls, cluster-mtls, sentinel-standalone0.
3.2 The Java side (`src/test/java/redis/clients/jedis/`)
| Class | Role |
|---|---|
Endpoints |
Loads the JSON at class-init; getRedisEndpoint(name) returns an EndpointConfig (or throws TestAbortedException → test skipped, not failed, when the endpoint is absent). |
EndpointConfig |
Parsed entry (Gson, LOWER_CASE_WITH_UNDERSCORES). Provides getHostAndPort(), getHostsAndPorts(), getPassword(), isTls(), getCertificatesLocation(), and getClientConfigBuilder() (pre-wired credentials + SSL socket factory). |
util/TestEnvUtil |
Chooses which endpoints file to load (see §6). |
util/TlsUtil |
Builds a JCEKS truststore from the tls_cert_path cert dir(s) and sets javax.net.ssl.trustStore* system properties. |
Typical usage:
@BeforeAll static void setUp() {
endpoint = Endpoints.getRedisEndpoint("standalone0-tls");
}
// ...
try (Jedis j = new Jedis(endpoint.getHostAndPort(), endpoint.getClientConfigBuilder().build())) { ... }mTLS / TLS tests build a truststore from the cert location(s):
trustStorePath = TlsUtil.createAndSaveTestTruststore(name, List.of(endpoint.getCertificatesLocation()), "changeit");
TlsUtil.setCustomTrustStore(trustStorePath, "changeit");4. Running the tests (Makefile)
| Command | Effect |
|---|---|
make start version=8.6 |
docker compose … up -d --wait with .env + .env.v8.6. Supported: 6.2, 7.2, 7.4, 8.0, 8.2, 8.4, 8.6, 8.8, 8.10, 8.12 (milestone 8.12-m02-int; default 8.10). |
make stop |
docker compose … down. |
make test [version=…] |
start → mvn clean verify → stop. |
mvn clean verify |
Run tests against an already-started env (use this from the IDE between make start/make stop). |
Custom image instead of a version: make start CLIENT_LIBS_TEST_IMAGE_TAG=<tag>.
Test selection (JUnit 5 tags + Maven, see `pom.xml`)
| Group | Tag / naming | Runner | Default |
|---|---|---|---|
| Unit | @Tag("unit"), no network |
Surefire | run (skipUnitTests=false) |
| Integration | *IT (new) — legacy *IntegrationTest / @Tag("integration") still run |
Failsafe | run (skipIntegrationTests=false) |
| Scenario | @Tag("scenario") |
Failsafe | skipped (skipScenarioTests=true) |
New integration tests must be named *IT — do not use *IntegrationTest or
the @Tag("integration") marker. See §5.
Useful flags: -DskipUnitTests=true (CI Docker job runs integration only),-Dtest=<Class#method> to narrow.
5. Test source layout & where to put tests
All test code lives under src/test/java/redis/clients/jedis/. The package
root holds the broad client/pool/connection tests; everything else is grouped by
area:
src/test/java/redis/clients/jedis/
├── commands/ # command coverage (integration)
│ ├── jedis/ # against the legacy Jedis API
│ ├── unified/ # against the UnifiedJedis surface
│ └── commandobjects/
├── search/ json/ timeseries/ # module command tests (integration)
├── tls/ # TLS / mTLS / SSL options (integration, *IT.java)
├── csc/ # client-side caching (integration)
├── failover/ # multi-db / failover (integration, @Tag("failover"))
├── authentication/ # auth + token-based auth
├── mcf/ providers/ executors/ prefix/ # connection/provider internals
├── params/ builders/ resps/ # mostly unit (arg/response objects)
├── mocked/ # pure unit, Mockito-based — no server
├── scenario/ # @Tag("scenario") — long-running, skipped by default
├── examples/ # io.redis.examples — excluded from test runs
├── benchmark/ codegen/ # excluded from test runs
└── util/ # shared test helpers (TestEnvUtil, TlsUtil, …) — not testsUnit vs. integration: the rule
What decides whether a test is run by Surefire (unit) or Failsafe (integration)
is how the test is marked — its file-name suffix and/or JUnit tag — not which
folder it sits in (the runners select by suffix and tag — see §4 and pom.xml).
New integration tests MUST be named *IT. This is the single accepted
convention for new code. Failsafe still recognises the older *IntegrationTest
name and the @Tag("integration") marker so the many existing classes keep
running while they are migrated, but do not use them for new tests — name
your class FooIT and nothing else.
| Test needs… | Name / mark it as… | Runner |
|---|---|---|
| No server (pure logic, mocks) | a plain *Test / *Tests class, untagged |
Surefire |
| A running Redis | *IT file name |
Failsafe |
| A long-running scenario | @Tag("scenario"), placed under scenario/ |
Failsafe (off by default) |
Guidance for new tests:
- Integration test → name the file
FooIT.java(the only accepted name
for new integration tests — not*IntegrationTest, and never@Tag("integration")). Put it in the area package it exercises (commands/…,search/,tls/, etc.). It connects to a server resolved throughEndpoints.getRedisEndpoint(...). - Unit test → add it next to related unit tests (or under
mocked/if it is
Mockito-based), name itFooTest, and leave it untagged. It must pass with no
Redis running. - Never put production-required fixtures in
examples/,benchmark/, orcodegen/— those packages are excluded from the test run. Shared helpers go
inutil/(helpers there are not collected as tests even if named*Test).
6. Two environments: Docker (full) vs. local-from-source (limited)
Selection is via the TEST_ENV_PROVIDER env var, read by TestEnvUtil:
| Provider | Endpoints file | Servers | Use |
|---|---|---|---|
oss-docker (default) |
endpoints.json |
Full compose matrix (standalone, sentinel, cluster, TLS, mTLS, stack) | Full integration coverage. |
oss-source |
endpoints_source.json |
3 plain redis-server processes started by make start-local: standalone0 (6379, ACL users), a Unix-socket instance, and one deliberately-unavailable instance |
Lightweight local dev; limited subset. |
re |
(REDIS_ENDPOINTS_CONFIG_PATH) |
Redis Enterprise | Internal. |
Override the endpoints file directly with REDIS_ENDPOINTS_CONFIG_PATH, and the
TLS cert root with TEST_WORK_FOLDER (default /tmp/redis-env-work).
Local-from-source flow
make system-setup # CI only: build Redis from github.com/redis/redis + the test module (testmodule.so)
make test-local # = start-local (TEST_ENV_PROVIDER=oss-source) → mvn verify → stop-localmake start-local runs three local redis-server instances and make mvn-test-local forces TEST_ENV_PROVIDER=oss-source, so tests loadendpoints_source.json. Endpoints absent there (cluster, sentinel, TLS, mTLS,
stack) resolve to a TestAbortedException and are skipped — that is why the
local env runs only a subset. Full coverage requires the Docker env.
7. CI workflows (`.github/workflows/`)
| Workflow | Environment | What it does |
|---|---|---|
test-on-docker.yml |
Docker (full) | Matrix over Redis 7.2 → 8.12. Uses the run-tests composite action: make start version=<v> → TEST_ENV_PROVIDER=oss-docker, TEST_WORK_FOLDER=$REDIS_ENV_WORK_DIR, mvn -B -DskipUnitTests=true clean compile verify → make stop. Uploads JaCoCo + Surefire/Failsafe reports. |
integration.yml |
Local-from-source (limited) | Java 8; make system-setup builds Redis from source + the test module; make test-local runs the oss-source subset. |
Both clients pin Java 8 for the test build. Use it when reproducing CI.
Jedis Maven (`docs/jedis-maven.md`)
Use Jedis as a maven dependency:
Official Releases
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>8.0.1</version>
</dependency>Snapshots
<repositories>
<repository>
<id>central-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>and
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>8.1.0-SNAPSHOT</version>
</dependency>
</dependencies>Redisearch (`docs/redisearch.md`)
RediSearch Jedis Quick Start
To use RediSearch features with Jedis, you'll need to use an implementation of RediSearchCommands.
Creating the RediSearch client
Initializing the client with RedisClient:
RedisClient client = RedisClient.builder().hostAndPort("localhost", 6379).build();Initializing the client with RedisClusterClient:
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("127.0.0.1", 7379));
nodes.add(new HostAndPort("127.0.0.1", 7380));
RedisClusterClient client = RedisClusterClient.builder().nodes(nodes).build();Indexing and querying
Indexing
Defining a schema for an index and creating it:
Schema sc = new Schema()
.addTextField("title", 5.0)
.addTextField("body", 1.0)
.addNumericField("price");
IndexDefinition def = new IndexDefinition()
.setPrefixes(new String[]{"item:", "product:"})
.setFilter("@price>100");
client.ftCreate("item-index", IndexOptions.defaultOptions().setDefinition(def), sc);Alternatively, we can create the same index using FTCreateParams:
client.ftCreate("item-index",
FTCreateParams.createParams()
.prefix("item:", "product:")
.filter("@price>100"),
TextField.of("title").weight(5.0),
TextField.of("body"),
NumericField.of("price")
);Inserting
Adding documents to the index:
Map<String, Object> fields = new HashMap<>();
fields.put("title", "hello world");
fields.put("state", "NY");
fields.put("body", "lorem ipsum");
fields.put("price", 1337);
client.hset("item:hw", RediSearchUtil.toStringMap(fields));Another way to insert documents:
client.hsetObject("item:hw", fields);Querying
Searching the index:
Query q = new Query("hello world")
.addFilter(new Query.NumericFilter("price", 0, 1000))
.limit(0, 5);
SearchResult sr = client.ftSearch("item-index", q);Alternative searching using FTSearchParams:
SearchResult sr = client.ftSearch("item-index",
"hello world",
FTSearchParams.searchParams()
.filter("price", 0, 1000)
.limit(0, 5));Aggregation query:
AggregationBuilder ab = new AggregationBuilder("hello")
.apply("@price/1000", "k")
.groupBy("@state", Reducers.avg("@k").as("avgprice"))
.filter("@avgprice>=2")
.sortBy(10, SortedField.asc("@state"));
AggregationResult ar = client.ftAggregate("item-index", ab);Redisjson (`docs/redisjson.md`)
RedisJSON Jedis Quick Start
Jedis supports RedisJSON and RediSearch.
The latest versions of RedisJSON let you store, manipulate, index, and query JSON.
To use these features with Jedis, you'll need to use the UnifiedJedis interface or a sub-class of it.
Let's see how this works.
Creating with RedisJSON client
First, let's create a RedisClient client instance:
RedisClient client = RedisClient.builder().hostAndPort("localhost", 6479).build();Or, a RedisClusterClient client instance:
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("127.0.0.1", 7379));
nodes.add(new HostAndPort("127.0.0.1", 7380));
RedisClusterClient client = RedisClusterClient.builder().nodes(nodes).build();Now we can start working with JSON. For these examples, we'll be using GSON
to handle the serialization of POJOs to JSON.
Creating JSON documents
Suppose we're building an online learning platform, and we want to represent students.
Let's create a POJO to represent our students:
private class Student {
private String firstName;
private String lastName;
public Student(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}Now we can create some students and store them in Redis as JSON:
final Gson gson = new Gson();
Student maya = new Student("Maya", "Jayavant");
client.jsonSet("student:111", gson.toJson(maya));
Student oliwia = new Student("Oliwia", "Jagoda");
client.jsonSet("student:112", gson.toJson(oliwia));Some of other ways to store POJOs as JSON:
client.jsonSetLegacy("student:111", maya);
client.jsonSetWithEscape("student:112", oliwia);Querying and indexing JSON
If we want to be able to query this JSON, we'll need to create an index. Let's create an index on the "firstName" and "lastName" fields.
- We define which fields to index ("firstName" and "lastName").
- We set up the index definition to recognize JSON and include only those documents whose key starts with "student:".
- Then we actually create the index, called "student-index", by calling
ftCreate().
Schema schema = new Schema().addTextField("$.firstName", 1.0).addTextField("$.lastName", 1.0);
IndexDefinition rule = new IndexDefinition(IndexDefinition.Type.JSON)
.setPrefixes(new String[]{"student:"});
client.ftCreate("student-index", IndexOptions.defaultOptions().setDefinition(rule), schema);Alternatively creating the same index using FTCreateParams:
client.ftCreate("student-index",
FTCreateParams.createParams().on(IndexDataType.JSON).prefix("student:"),
TextField.of("$.firstName"), TextField.of("$.lastName"));With an index now defined, we can query our JSON. Let's find all students whose name begins with "maya":
Query q = new Query("@\\$\\.firstName:maya*");
SearchResult mayaSearch = client.ftSearch("student-index", q);Same query can be done using FTSearchParams:
SearchResult mayaSearch = client.ftSearch("student-index",
"@\\$\\.firstName:maya*",
FTSearchParams.searchParams());We can then iterate over our search results:
List<Document> docs = mayaSearch.getDocuments();
for (Document doc : docs) {
System.out.println(doc);
}This example just scratches the surface. You can atomically manipulate JSON documents and query them in a variety of ways.
See the RedisJSON docs, the RediSearch docs,
and our course, "Querying, Indexing, and Full-text Search in Redis",
for a lot more examples.
Release Process (`docs/release-process.md`)
Release Process
Releases are drafted automatically by
Release Drafter
and deployed by the
Release workflow
when a draft is published.
Branches
| Branch | Hosts | Example |
|---|---|---|
master |
next minor or major | 8.1.0-SNAPSHOT |
MAJOR.x |
MAJOR.0.PATCH |
7.x, 8.x |
MAJOR.MINOR.x |
MAJOR.MINOR.PATCH |
7.5.x, 8.1.x |
- Minor and major releases, including betas, are tagged on
master. - Patch releases are tagged on their maintenance branch, never on
master. - A maintenance branch is cut from the GA tag commit (
8.1.xfromv8.1.0). - Fixes land on
masterfirst and are backported with a PR titledBackport # to <line>: <original title>, carrying the original labels.
How the draft is produced
On every push to master or a maintenance branch, Release Drafter updates one
draft per branch:
- Version is derived from the nearest GA tag reachable from the branch,
ignoring prerelease tags: patch bump on maintenance branches, minor bump onmaster. Example:8.xatv8.0.1drafts8.0.2;masteratv8.0.0
drafts8.1.0. - Changes are the pull requests merged to the branch since the latest GA
release published from that branch. For the first patch after a branch is
cut, the GA tag's commit date is used as the cutoff instead. - Betas are prereleases and are not used as a base, so GA notes are cumulative
since the previous GA. - PRs labeled
skip-changelogorchurnare excluded. Unlabeled PRs appear
uncategorized, so label backports.
Publishing
- Review the draft on the Releases page and edit the notes if needed.
- Publish from the draft. This keeps the release target on the right branch,
which the next draft on that branch depends on. Do not create releases
from the tag page. - For a patch of an older line, untick Set as the latest release.
- Publishing creates the tag and triggers the Release workflow, which sets
the Maven version from the tag and deploys to Maven Central.
Major releases and betas are not derived automatically. Run the workflow
manually with the version input, for example 9.0.0 or 8.1.0-beta2.
Version bumps
The Release workflow sets the artifact version from the tag, so the pom.xml
version on a maintenance branch does not affect published artifacts. Bump the
snapshot version on master only after a minor or major release, with the PR
labeled skip-changelog.
Requirements (`docs/requirements.txt`)
mkdocs~=1.6
mkdocs-material~=9.5
pymdown-extensions~=10.8
mkdocs-macros-plugin~=1.0
mkdocs-glightbox
Smart Client Handoffs (`docs/smart-client-handoffs.md`)
Smart Client Handoffs (Maintenance Notifications)
Overview
Smart client handoffs (SCH) is a feature of
Redis Cloud and Redis Software servers that notifies clients about planned maintenance — shard
migrations, shard failovers, and endpoint rebinds — shortly before it happens, so the client can
respond gracefully instead of hitting command timeouts and dropped connections. The notifications
arrive as RESP3 push messages, and Jedis reacts on the client side:
- Relaxed timeouts — while a maintenance operation is in progress, socket read timeouts are
temporarily loosened so in-flight and new commands survive the operation instead of timing out. - Pre-handoffs — when an endpoint moves (
MOVING), the pool proactively directs new
connections to the new endpoint and retires connections to the old one within the announced grace
period, instead of waiting for a hard disconnect.
The feature is negotiated per connection during the handshake withCLIENT MAINT_NOTIFICATIONS ON. Servers that do not support the command are unaffected (see
Modes).
Requirements
- RESP3. Push messages require the RESP3 protocol. On a RESP2 connection the feature stays
inactive inAUTOmode and fails the connection setup inENABLEDmode. - Server support. The server must support the
CLIENT MAINT_NOTIFICATIONScommand; see
SCH support in Redis server products
for availability and enablement per product. - Client.
RedisClient(standalone) supports the feature and enables it automatically on RESP3
connections.
Enabling
With RedisClient, using RESP3 is enough — the default mode is AUTO:
RedisClient client = RedisClient.builder()
.hostAndPort(new HostAndPort("localhost", 6379))
.clientConfig(DefaultJedisClientConfig.builder()
.protocol(RedisProtocol.RESP3)
.build())
.build();To customize the behavior, or turn the feature off:
RedisClient client = RedisClient.builder()
.hostAndPort(new HostAndPort("localhost", 6379))
.clientConfig(DefaultJedisClientConfig.builder()
.protocol(RedisProtocol.RESP3)
.build())
.maintenanceNotifications(MaintenanceNotificationsConfig.builder()
.mode(MaintenanceNotificationsConfig.Mode.ENABLED) // fail setup if unsupported
.relaxedTimeout(30_000) // non-blocking commands, millis
.relaxedBlockingTimeout(0) // blocking commands; 0 = infinite
.build())
.build();
// off:
RedisClient plain = RedisClient.builder()
.hostAndPort(new HostAndPort("localhost", 6379))
.maintenanceNotifications(MaintenanceNotificationsConfig.DISABLED)
.build();Modes
| Mode | Behavior |
|---|---|
AUTO (default for RedisClient) |
The handshake is attempted on RESP3 connections; if the server rejects it (or the connection is RESP2), the feature is quietly disabled for that connection. |
ENABLED |
The handshake must succeed: connection setup fails if the server rejects CLIENT MAINT_NOTIFICATIONS or the connection is not RESP3. |
DISABLED |
The handshake is not attempted; the feature is off. |
Notifications and how the client reacts
| Notification | Wire format | Meaning | Client reaction |
|---|---|---|---|
MIGRATING |
["MIGRATING", seq, startsInSeconds, shard-ids] |
A shard migration starts within the lead time. | Relax the receiving connection's timeouts. |
MIGRATED |
["MIGRATED", seq, shard-ids] |
The migration completed. | Restore the receiving connection's timeouts. |
FAILING_OVER |
["FAILING_OVER", seq, startsInSeconds, shard-ids] |
A shard failover starts within the lead time. | Relax the receiving connection's timeouts. |
FAILED_OVER |
["FAILED_OVER", seq, shard-ids] |
The failover completed. | Restore the receiving connection's timeouts. |
MOVING |
["MOVING", seq, graceSeconds, "host:port"] |
The endpoint moves to the target; connections to the old endpoint hard-disconnect after the grace period. | Relax timeouts pool-wide, direct new connections to the target, and retire connections to the old endpoint (see below). |
Relaxed timeouts
While a maintenance window is open, the connection uses the looser of the relaxed value and the
configured socket timeout (0 = infinite is the loosest) — relaxation never tightens a timeout.
Non-blocking and blocking commands have separate relaxed values (relaxedTimeout, default 10 s, andrelaxedBlockingTimeout, default 0 = infinite).
A window opened by MIGRATING/FAILING_OVER normally closes with the matchingMIGRATED/FAILED_OVER. If the closing notification is lost, the window reverts on its own afterrelaxedWindowMaxDuration (default 60 s). A MOVING window has no closing notification: relaxation
applies to every pool connection toward the affected endpoint and ends when the announced grace
period expires.
Pre-handoff (`MOVING`)
During the grace period:
- Commands in flight on existing connections complete normally under the relaxed timeouts.
- New connections — including replacements for returned ones — are opened against the endpoint
carried by the notification and inherit the relaxed timeouts. - Connections to the old endpoint are retired: each is discarded when returned to the pool.
- A connection held past the grace period is disconnected by the server; using it throws
JedisConnectionException, and returning it replaces it with a fresh connection.
Endpoint types
The client tells the server which address format to return in MOVING. By default this is
auto-resolved per connection: a private remote IP requests INTERNAL_*, a public one EXTERNAL_*;
TLS requests *_FQDN, plaintext *_IP. A fixed type can be requested withMaintenanceNotificationsConfig.builder().endpointType(...), including NONE — the notification
then carries no target and the client schedules a reconnect to its configured endpoint at half the
grace period.
When choosing a fixed type, make sure the requested address family is routable from where the
client runs — an internal IP is typically not reachable from outside the cluster's network.
See the official SCH documentation for the
cross-client overview, server-side enablement, and product-specific limitations.
Pub/Sub
A subscribed connection consumes maintenance notifications inline between pub/sub messages, so
relaxed timeouts apply to it like to any other connection. Pre-handoff, however, does not: the
subscribed connection is held by the listen loop and cannot be replaced underneath the
application, so on MOVING it stays on the old endpoint and is disconnected by the server when
the grace period expires. The subscription then fails with JedisConnectionException;
re-subscribing — on a fresh connection, which reaches the new endpoint — is the application's
responsibility.
Production notes
- JVM DNS caching. After the grace period the client reconnects using the configured endpoint
name. The JVM caches successful DNS lookups for a fixed 30 seconds by default (ignoring record
TTLs), which can outlive the grace period. Disable the cache or set it to a low value via thenetworkaddress.cache.ttl
security property.
Transactions Multi (`docs/transactions-multi.md`)
Transactions/Multi
Overview
Transactions guarantee that all the included commands will execute to completion without being interrupted by commands from other clients. See the Transactions page for more information.
To execute commands in a transaction, create a transaction object with the multi() command, call command methods on that object, and then call the transaction object's exec() method to execute it. You can access the results from commands in the transaction using Response objects. The exec() method also returns a List<Object> value that contains all the result values in the order the commands were executed.
Immediate Transaction Start
The simplest way to use transactions is to call multi() on your Jedis client, which immediately starts a transaction by sending the MULTI command to Redis. All subsequent commands are queued until exec() is called.
import redis.clients.jedis.RedisClient;
import redis.clients.jedis.AbstractTransaction;
import redis.clients.jedis.Response;
import java.util.List;
RedisClient jedis = RedisClient.create("redis://localhost:6379");
// Create a transaction that immediately sends MULTI
try (AbstractTransaction tx = jedis.multi()) {
// Commands are queued
Response<String> set1 = tx.set("counter:1", "0");
Response<Long> incr1 = tx.incrBy("counter:1", 1);
Response<Long> incr2 = tx.incrBy("counter:1", 2);
// Execute the transaction
List<Object> results = tx.exec();
// Access results via Response objects
System.out.println(incr1.get()); // 1
System.out.println(incr2.get()); // 3
// Or via the results list
System.out.println(results.get(0)); // OK
System.out.println(results.get(1)); // 1
System.out.println(results.get(2)); // 3
}
jedis.close();Response Handling
Commands invoked within a transaction return Response<T> objects. These responses become available only after exec() is called:
- Before
exec(): Callingresponse.get()will throwIllegalStateExceptionwith the message "Please close pipeline or multi block before calling this method." - After
exec(): Response objects contain the actual results from Redis
The exec() method returns a List<Object> containing all command results in the order they were queued.
Manual Transaction Start
For more control, you can create a transaction without immediately sending MULTI. This is useful when you need to:
- Execute commands before starting the transaction
- Use
WATCHto implement optimistic locking - Conditionally start the transaction
Create a manual transaction by passing false to the transaction() method:
RedisClient jedis = RedisClient.create("redis://localhost:6379");
// Create transaction without sending MULTI
try (AbstractTransaction tx = jedis.transaction(false)) {
// Commands before multi() are executed immediately
Response<String> setBeforeMulti = tx.set("mykey", "initial_value");
Response<String> getBeforeMulti = tx.get("mykey");
// These responses are available immediately
System.out.println(setBeforeMulti.get()); // OK
System.out.println(getBeforeMulti.get()); // initial_value
// Now start the transaction
tx.multi();
// Commands after multi() are queued
Response<String> set = tx.set("mykey", "new_value");
Response<String> get = tx.get("mykey");
// Execute the transaction
List<Object> results = tx.exec();
// Results from queued commands
System.out.println(set.get()); // OK
System.out.println(get.get()); // new_value
}
jedis.close();Using WATCH for Optimistic Locking
The WATCH command monitors keys for changes. If any watched key is modified before EXEC, the transaction is aborted and exec() returns null.
Important: WATCH must be executed through the transaction object to ensure it uses the transaction's dedicated connection.
try (AbstractTransaction tx = jedis.transaction(false)) {
// WATCH must be on the transaction's dedicated connection
tx.watch("counter");
// Read current value - can use the client directly
String current = jedis.get("counter");
int newValue = Integer.parseInt(current) + 1;
// Start transaction and queue update
tx.multi();
tx.set("counter", String.valueOf(newValue));
// Returns null if key was modified by another client
List<Object> results = tx.exec();
if (results == null) {
System.out.println("Transaction aborted - key was modified");
}
}Connection Lifecycle
A transaction acquires a dedicated connection that is held until close() is called. Ensure the transaction is closed on error:
try (AbstractTransaction tx = jedis.multi()) {
tx.set("key", "value");
tx.exec();
}Important: WATCH must be executed through the transaction object to ensure it uses the transaction's dedicated connection.
Transaction Completion
Complete a transaction by calling either:
exec()- Executes all queued commands atomically and returns aList<Object>with the resultsdiscard()- Discards all queued commands without executing them
Automatic Cleanup
When using try-with-resources, close() automatically sends DISCARD (if in MULTI state) or UNWATCH (if in WATCH state) to ensure the connection is returned to the pool in a clean state.
Notes on `MultiDbTransaction`
MultiDbTransaction (returned by multi() / transaction(...) on a multi-database client) is annotated @Experimental and behaves slightly differently from the single-endpoint transaction.
Connection Lifecycle
- When started with
multi()(i.e.doMulti=true), connection acquisition is deferred untilexec()(ordiscard()/close()). Commands issued afterMULTIare buffered in memory only. - When started with
transaction(false)(i.e.doMulti=false), a connection is acquired on the first command that needs to be sent to the server (e.g.watch(...)or any pre-MULTIread/write) and held until the transaction is completed. - A
MultiDbTransactionis bound to the database that was active at the time the connection was acquired, and the connection is borrowed from that database's pool. Subsequent database failovers do not migrate the in-flight transaction to the new endpoint. - On
exec(),discard()andclose()the held connection is released back to its originating pool (viaConnection.close()) in thefinallyblock, regardless of success or failure. The next operation after release will use a fresh connection from the currently active database.
Database-switch Semantics
If a failover changes the active database while a MultiDbTransaction is in progress, the transaction will not silently complete against a different endpoint:
- For commands sent before
MULTI(pre-MULTItraffic on a manual transaction), the nextappendCommandcall detects the switch and throwsJedisException("Active database has changed since transaction started")without sending the command. - On
exec(), after queueing commands on the server, the active database is re-checked. If it has changed, the transaction sendsDISCARDto the original connection so the server-sideMULTIblock is rolled back, then throwsJedisException("Active database has changed since transaction started"). Any error from theDISCARDattempt is attached as a suppressed exception. - In both cases the held connection is still released via the normal
finally-block path, so it is returned to its (original) pool and subsequent operations go through the now-active database.
Tutorials Examples (`docs/tutorials_examples.md`)
Tutorials and Examples
General
- Redis for Java Developers: https://university.redis.io/learningpath/kllayn0wtd847i
- Jedis Guide: https://redis.io/docs/latest/develop/clients/jedis/
- Connecting to a Redis server: https://redis.io/docs/latest/develop/clients/jedis/connect/
- Using Jedis in production: https://redis.io/docs/latest/develop/clients/jedis/produsage/
Client-side Caching
- Client-side Caching: https://github.com/Redislabs-Solution-Architects/redis-client-side-caching-csc-jedis-demo
JSON
- Store, Read and Search JSON: https://redis.io/kb/doc/1cd7hi2721/learn-to-store-read-and-search-data-in-json-documents-using-jedis
Search
- Vector Search: https://redis.io/kb/doc/13qsrk8xpx/how-to-perform-vector-search-in-java-with-the-jedis-client-library
- Spring Boot Search: https://github.com/Redislabs-Solution-Architects/Spring-Boot-RediSearch-Example
Verifying Artifacts (`docs/verifying-artifacts.md`)
Verifying contents
Jedis artifacts published on Maven central are signed. For each artifact, there is an associated signature file with the .asc suffix.
Keys used for signing
pub rsa3072 2023-12-17 [SC]
165E63A75659104C72B49129C2B44BE148BDCEC8
uid [ unknown] Redis OSS <[email protected]>
sig 3 C2B44BE148BDCEC8 2023-12-17 [self-signature]A copy of this key is stored on the keyserver Ubuntu keyserver
Before 2023-12-17
pub rsa3072 2023-09-03 [SC]
6BF6C1905C9642D1181D157443543D884CC71C96
uid [ unknown] Redis OSS <[email protected]>
sig 3 43543D884CC71C96 2023-09-03 [self-signature]A copy of this key is stored on the keyserver Ubuntu keyserver
Before 2023-09-03
pub rsa3072 2021-06-27 [SC] [expired: 2023-06-27]
1C913D6D36DAD0A679863F5CB68CD0A11D28B97D
uid [ expired] RedisLabs OSS <[email protected]>
sig 3 B68CD0A11D28B97D 2021-06-27 [self-signature]A copy of this key is stored on the keyserver Ubuntu keyserver
Before 2021-06-27
pub rsa3072 2018-08-12 [SC] [expired: 2020-08-11]
4EC78BD38D30A2E85C02C39C2D62B50EF8D3297A
uid [ expired] Guy Korlad <[email protected]>
sig 3 2D62B50EF8D3297A 2018-08-12 [self-signature]A copy of this key is stored on the keyserver Ubuntu keyserver
Migration Guides V3 To V4 Primitives (`docs/migration-guides/v3-to-v4-primitives.md`)
The following methods now return primitive values:
>> long/boolean/double instead of Long/Boolean/Double:
- dbSize()
- lastsave()
- slowlogLen()
- clientId()
- clientUnblock(long clientId, UnblockType unblockType)
- clientKill(ClientKillParams params)
- aclDelUser(String name)
- aclDelUser(byte[] name)
- move(String key, int dbIndex)
- move(byte[] key, int dbIndex)
- waitReplicas(int replicas, long timeout)
- waitReplicas(String key, int replicas, long timeout)
- exists(String key)
- exists(byte[] key)
- exists(String... keys)
- exists(byte[]... keys)
- persist(String key)
- expire(String key, long seconds)
- expire(byte[] key, long seconds)
- pexpire(String key, long milliseconds)
- pexpire(byte[] key, long milliseconds)
- expireAt(String key, long unixTime)
- expireAt(byte[] key, long unixTime)
- pexpireAt(String key, long millisecondsTimestamp)
- pexpireAt(byte[] key, long millisecondsTimestamp)
- ttl(String key)
- ttl(byte[] key)
- pttl(String key)
- touch(String key)
- touch(byte[] key)
- touch(String... keys)
- touch(byte[]... keys)
- setbit(String key, long offset, boolean value)
- setbit(byte[] key, long offset, boolean value)
- getbit(String key, long offset)
- getbit(byte[] key, long offset)
- setrange(String key, long offset, String value)
- setrange(byte[] key, long offset, byte[] value)
- setnx(String key, String value)
- setnx(byte[] key, byte[] value)
- incr(String key)
- incr(byte[] key)
- decr(String key)
- decr(byte[] key)
- incrBy(String key, long increment)
- incrBy(byte[] key, long increment)
- decrBy(String key, long decrement)
- decrBy(byte[] key, long decrement)
- incrByFloat(String key, double increment)
- incrByFloat(byte[] key, double increment)
- append(String key, String value)
- append(byte[] key, byte[] value)
- hset(String key, String field, String value)
- hset(byte[] key, byte[] field, byte[] value)
- hset(String key, Map<String, String> hash)
- hset(byte[] key, Map hash)
- hsetnx(String key, String field, String value)
- hsetnx(byte[] key, byte[] field, byte[] value)
- hincrBy(String key, String field, long value)
- hincrBy(byte[] key, byte[] field, long value)
- hincrByFloat(String key, String field, double value)
- hincrByFloat(byte[] key, byte[] field, double value)
- hexists(String key, String field)
- hexists(byte[] key, byte[] field)
- hdel(String key, String... field)
- hdel(byte[] key, byte[]... field)
- hlen(String key)
- hlen(byte[] key)
- rpush(String key, String... string)
- rpush(byte[] key, byte[]... args)
- lpush(String key, String... string)
- lpush(byte[] key, byte[]... args)
- llen(String key)
- llen(byte[] key)
- lrem(String key, long count, String value)
- lrem(byte[] key, long count, byte[] value)
- sadd(String key, String... member)
- sadd(byte[] key, byte[]... member)
- scard(String key)
- scard(byte[] key)
- sismember(String key, String member)
- sismember(byte[] key, byte[] member)
- strlen(String key)
- strlen(byte[] key)
- zadd(String key, double score, String member)
- zadd(byte[] key, double score, byte[] member)
- zadd(String key, double score, String member, ZAddParams params)
- zadd(byte[] key, double score, byte[] member, ZAddParams params)
- zadd(String key, Map<String, Double> scoreMembers)
- zadd(byte[] key, Map scoreMembers)
- zadd(String key, Map<String, Double> scoreMembers, ZAddParams params)
- zadd(byte[] key, Map scoreMembers, ZAddParams params)
- zrem(String key, String... members)
- zrem(byte[] key, byte[]... members)
- zincrby(String key, double increment, String member)
- zincrby(byte[] key, double increment, byte[] member)
- zcard(String key)
- zcard(byte[] key)
- zcount(String key, double min, double max)
- zcount(byte[] key, double min, double max)
- zcount(String key, String min, String max)
- zcount(byte[] key, byte[] min, byte[] max)
- zremrangeByRank(String key, long start, long stop)
- zremrangeByRank(byte[] key, long start, long stop)
- zremrangeByScore(String key, double min, double max)
- zremrangeByScore(byte[] key, double min, double max)
- zremrangeByScore(String key, String min, String max)
- zremrangeByScore(byte[] key, byte[] min, byte[] max)
- zlexcount(String key, String min, String max)
- zlexcount(byte[] key, byte[] min, byte[] max)
- zremrangeByLex(String key, String min, String max)
- zremrangeByLex(byte[] key, byte[] min, byte[] max)
- linsert(String key, ListPosition where, String pivot, String value)
- linsert(byte[] key, ListPosition where, byte[] pivot, byte[] value)
- lpushx(String key, String... string)
- lpushx(byte[] key, byte[]... arg)
- rpushx(String key, String... string)
- rpushx(byte[] key, byte[]... arg)
- del(String key)
- del(byte[] key)
- del(String... keys)
- unlink(String key)
- unlink(byte[] key)
- unlink(String... keys)
- bitcount(String key)
- bitcount(byte[] key)
- bitcount(String key, long start, long end)
- bitcount(byte[] key, long start, long end)
- bitpos(String key, boolean value)
- bitpos(String key, boolean value, BitPosParams params)
- pfadd(String key, String... elements)
- pfadd(byte[] key, byte[]... elements)
- pfcount(byte[]... keys)
- geoadd(String key, double longitude, double latitude, String member)
- geoadd(byte[] key, double longitude, double latitude, byte[] member)
- geoadd(String key, Map<String, GeoCoordinate> memberCoordinateMap)
- geoadd(byte[] key, Map memberCoordinateMap)
- geoadd(String key, GeoAddParams params, Map<String, GeoCoordinate> memberCoordinateMap)
- geoadd(byte[] key, GeoAddParams params, Map memberCoordinateMap)
- hstrlen(String key, String field)
- hstrlen(byte[] key, byte[] field)
- xlen(String key)
- xlen(byte[] key)
- xack(String key, String group, StreamEntryID... ids)
- xack(byte[] key, byte[] group, byte[]... ids)
- xgroupDestroy(String key, String groupname)
- xgroupDestroy(byte[] key, byte[] consumer)
- xgroupDelConsumer( String key, String groupname, String consumername)
- xgroupDelConsumer(byte[] key, byte[] consumer, byte[] consumerName)
- xdel(String key, StreamEntryID... ids)
- xdel(byte[] key, byte[]... ids)
- xtrim(String key, long maxLen, boolean approximate)
- xtrim(byte[] key, long maxLen, boolean approximateLength)
- xtrim(String key, XTrimParams params)
- xtrim(byte[] key, XTrimParams params)
- clusterKeySlot(String key)
- clusterCountKeysInSlot(int slot)
- msetnx(String... keysvalues)
- msetnx(byte[]... keysvalues)
- renamenx(String oldkey, String newkey)
- renamenx(byte[] oldkey, byte[] newkey)
- sdiffstore(String dstkey, String... keys)
- sdiffstore(byte[] dstkey, byte[]... keys)
- sinterstore(String dstkey, String... keys)
- sinterstore(byte[] dstkey, byte[]... keys)
- smove(String srckey, String dstkey, String member)
- smove(byte[] srckey, byte[] dstkey, byte[] member)
- sort(String key, String dstkey)
- sort(byte[] key, byte[] dstkey)
- sort(String key, SortingParams sortingParameters, String dstkey)
- sort(byte[] key, SortingParams sortingParameters, byte[] dstkey)
- sunionstore(String dstkey, String... keys)
- sunionstore(byte[] dstkey, byte[]... keys)
- zdiffStore(String dstkey, String... keys)
- zdiffStore(byte[] dstkey, byte[]... keys)
- zinterstore(String dstkey, String... sets)
- zinterstore(byte[] dstkey, byte[]... sets)
- zinterstore(String dstkey, ZParams params, String... sets)
- zinterstore(byte[] dstkey, ZParams params, byte[]... sets)
- zunionstore(String dstkey, String... sets)
- zunionstore(byte[] dstkey, byte[]... sets)
- zunionstore(String dstkey, ZParams params, String... sets)
- zunionstore(byte[] dstkey, ZParams params, byte[]... sets)
- bitop(BitOP op, String destKey, String... srcKeys)
- bitop(BitOP op, byte[] destKey, byte[]... srcKeys)
- georadiusStore(String key, double longitude, double latitude, double radius, GeoUnit unit, GeoRadiusParam param, GeoRadiusStoreParam storeParam)
- georadiusStore(byte[] key, double longitude, double latitude, double radius, GeoUnit unit, GeoRadiusParam param, GeoRadiusStoreParam storeParam)
- georadiusByMemberStore(String key, String member, double radius, GeoUnit unit, GeoRadiusParam param, GeoRadiusStoreParam storeParam)
- georadiusByMemberStore(byte[] key, byte[] member, double radius, GeoUnit unit, GeoRadiusParam param, GeoRadiusStoreParam storeParam)
- copy(String srcKey, String dstKey, int db, boolean replace)
- copy(byte[] srcKey, byte[] dstKey, int db, boolean replace)
- copy(String srcKey, String dstKey, boolean replace)
- copy(byte[] srcKey, byte[] dstKey, boolean replace)
Migration Guides V3 To V4 Zset List (`docs/migration-guides/v3-to-v4-zset-list.md`)
Each of the following sorted set methods now return a Java `List` instead of a `Set`:
- zrange(byte[] key, long start, long stop)
- zrange(String key, long start, long stop)
- zrevrange(byte[] key, long start, long stop)
- zrevrange(String key, long start, long stop)
- zrangeWithScores(byte[] key, long start, long stop)
- zrangeWithScores(String key, long start, long stop)
- zrevrangeWithScores(byte[] key, long start, long stop)
- zrevrangeWithScores(String key, long start, long stop)
- zrandmember(byte[] key, long count)
- zrandmember(String key, long count)
- zrandmemberWithScores(byte[] key, long count)
- zrandmemberWithScores(String key, long count)
- zpopmax(byte[] key, int count)
- zpopmax(String key, int count)
- zpopmin(byte[] key, int count)
- zpopmin(String key, int count)
- zrangeByScore(byte[] key, double min, double max)
- zrangeByScore(String key, double min, double max)
- zrangeByScore(byte[] key, byte[] min, byte[] max)
- zrangeByScore(String key, String min, String max)
- zrevrangeByScore(byte[] key, double max, double min)
- zrevrangeByScore(String key, double max, double min)
- zrangeByScore(byte[] key, double min, double max, int offset, int count)
- zrangeByScore(String key, double min, double max, int offset, int count)
- zrevrangeByScore(byte[] key, byte[] max, byte[] min)
- zrevrangeByScore(String key, String max, String min)
- zrangeByScore(byte[] key, byte[] min, byte[] max, int offset, int count)
- zrangeByScore(String key, String min, String max, int offset, int count)
- zrevrangeByScore(byte[] key, double max, double min, int offset, int count)
- zrevrangeByScore(String key, double max, double min, int offset, int count)
- zrangeByScoreWithScores(byte[] key, double min, double max)
- zrangeByScoreWithScores(String key, double min, double max)
- zrevrangeByScoreWithScores(byte[] key, double max, double min)
- zrevrangeByScoreWithScores(String key, double max, double min)
- zrangeByScoreWithScores(byte[] key, double min, double max, int offset, int count)
- zrangeByScoreWithScores(String key, double min, double max, int offset, int count)
- zrevrangeByScore(byte[] key, byte[] max, byte[] min, int offset, int count)
- zrevrangeByScore(String key, String max, String min, int offset, int count)
- zrangeByScoreWithScores(byte[] key, byte[] min, byte[] max)
- zrangeByScoreWithScores(String key, String min, String max)
- zrevrangeByScoreWithScores(byte[] key, byte[] max, byte[] min)
- zrevrangeByScoreWithScores(String key, String max, String min)
- zrangeByScoreWithScores(byte[] key, byte[] min, byte[] max, int offset, int count)
- zrangeByScoreWithScores(String key, String min, String max, int offset, int count)
- zrevrangeByScoreWithScores(byte[] key, double max, double min, int offset, int count)
- zrevrangeByScoreWithScores(String key, double max, double min, int offset, int count)
- zrevrangeByScoreWithScores(byte[] key, byte[] max, byte[] min, int offset, int count)
- zrevrangeByScoreWithScores(String key, String max, String min, int offset, int count)
- zrangeByLex(byte[] key, byte[] min, byte[] max)
- zrangeByLex(String key, String min, String max)
- zrangeByLex(byte[] key, byte[] min, byte[] max, int offset, int count)
- zrangeByLex(String key, String min, String max, int offset, int count)
- zrevrangeByLex(byte[] key, byte[] max, byte[] min)
- zrevrangeByLex(String key, String max, String min)
- zrevrangeByLex(byte[] key, byte[] max, byte[] min, int offset, int count)
- zrevrangeByLex(String key, String max, String min, int offset, int count)
Migration Guides V3 To V4 (`docs/migration-guides/v3-to-v4.md`)
Jedis 4 Breaking Changes
The
BinaryJedisandBinaryJedisClusterclasses have been removed.The methods from these classes are available in the
JedisandJedisClusterclasses
respectively.The following cases now throws an
IllegalStateExceptioninstead of aJedisDataException.Cannot use Jedis when in Multi. Please use Transaction or reset Jedis state.Cannot use Jedis when in Pipeline. Please use Pipeline or reset Jedis state.
The Redis transaction methods
multi(),exec()anddiscard()have
been removed fromPipeline.The
execGetResponse()method has been removed fromTransaction.The
watch()andunwatch()methods from theTransactionclass are unsupported
within MULTI (i.e., after themulti()method). However,watch()andunwatch ()can still be used before calling MULTI.The
JedisClusterconstructors withGenericObjectPoolConfig<Jedis>now acceptGenericObjectPoolConfig<Connection>.All
JedisClusterconstructors now throw aJedisClusterOperationExceptionif
unable to connect to any of the providedHostAndPort(s). Previously, the
connection would go into an unusable state.JedisCluster.getClusterNodes()returnsMap<String, ConnectionPool>instead ofMap<String, JedisPool>.JedisCluster.getConnectionFromSlot(int)returnsConnectioninstead ofJedis.JedisNoReachableClusterNodeExceptionhas been removed.JedisClusterOperationException, with a similar message, is thrown instead.JedisClusterMaxAttemptsExceptionhas been removed.JedisClusterOperationException, with a similar message, is thrown instead.JedisExhaustedPoolExceptionhas been removed. AJedisExceptionwith a similar message is thrown
instead.Many sorted set methods return a Java
Listinstead of aSet. See the complete list.Many methods return primitive values) (
long/boolean/doubleinstead ofLong/Boolean/Double). See the complete list.scriptExists(byte[])method now returnsBooleaninstead ofLong.scriptExists(byte[]...)method now returnsListinstead ofList<Long>.In the
xaddmethod with StreamEntryID
parameter, sending untypednullraises an exception.Casting the
nullto StreamEntryID ((StreamEntryID) null) resolves this issue.In the
xrangeandxrevrangemethods with StreamEntryID parameters, sending
untypednulls for both start and end parameters raises an exception.Casting the
nulls to StreamEntryID ((StreamEntryID) null) resolves this issue.The return type of
Jedis.shutdown()is nowvoid. Previously, it would return null.The
evalandevalshamethods are now non-blocking. These methods were
blocking in Jedis 3.x.The
HostAndPort.localhostconstant has been removed.The following methods have been removed from HostAndPort class:
extractParts()parseString()convertHost()setLocalhost()getLocalhost()getLocalHostQuietly()
The following classes have been moved to the
redis.clients.jedis.argspackage.BitOPGeoUnitListPosition
The following classes have been moved to the
redis.clients.jedis.paramspackage.BitPosParamsScanParamsSortingParamsZParams
The following classes have been moved to the
redis.clients.jedis.respspackage.AccessControlLogEntryAccessControlUserGeoRadiusResponseScanResultSlowlogStreamConsumersInfoStreamEntryStreamGroupInfoStreamInfoStreamPendingEntryStreamPendingSummaryTuple
Jedis and JedisPool constructors with a
Stringparameter, and noint
parameter, only support a URL or URI string.- Jedis(String)
- JedisPool(String)
- JedisPool(String, SSLSocketFactory, SSLParameters, HostnameVerifier)
- JedisPool(GenericObjectPoolConfig
, String)
The
ClientandBinaryClientclasses have been removed.redis.clients.jedis.commandspackage has been reimplemented, meaning that theCommandsinterfaces have been restructured.The
ShardedJedisPool,Sharded,ShardedJedis,BinaryShardedJedis,ShardInfo,JedisShardInfoclasses have been removed.Introduced
JedisShardingclass to replaceShardedJedisPool.Earlier code without the use of "name" and "weight" (in ShardInfo/JedisShardInfo) are
transferable to the new class.
ShardedJedisPipelineclass has been removed.- Introduced
ShardedPipelineclass to replaceShardedJedisPipeline.
- Introduced
The type of
Protocol.CHARSEThas been changed tojava.nio.charset.Charset.Jedis.debug(DebugParams)method has been removed.The
DebugParamsclass has been removed.The
Jedis.sync()method has been removed.The
Jedis.pubsubNumSub(String...)method now returnsMap<String, Long>
instead ofMap<String, String>.setDataSourcemethod in Jedis class now hasprotectedaccess.JedisPoolAbstractclass has been removed. UsePool<Jedis>.The
Pool.initPool()method has been removed.The
Pool.getNumActive()method now returns0(via GenericObjectPool) when the
pool is closed.The
Connection.getRawObjectMultiBulkReply()method has been removed. UseConnection.getUnflushedObjectMultiBulkReply()method.The
Queable.getResponse(Builder<T> builder)method has been renamed toQueable.enqueResponse(Builder<T> builder).All methods in Queable are now
final:clean()generateResponse(Object data)enqueResponse(Builder<T> builder)getPipelinedResponseLength()
These BuilderFactory implementations have been removed:
OBJECT(useRAW_OBJECT)BYTE_ARRAY_ZSET(useBINARY_LISTorBINARY_SET)BYTE_ARRAY_MAP(useBINARY_MAP)STRING_ZSET(useSTRING_LISTorSTRING_SET)EVAL_RESULT(useENCODED_OBJECT)EVAL_BINARY_RESULT(useRAW_OBJECT)
All String variables representing Cluster, Sentinel and PubSub subcommands in Protocol class
have been removed.ClientKillParams.Typehas been removed. UseClientType.ClusterResethas been removed. UseClusterResetType.The
JedisClusterHostAndPortMapinterface has been removed. Use theHostAndPortMapperinterface.JedisClusterHashTagUtilclass has been renamed toJedisClusterHashTag.The
KeyMergeUtilclass has been removed.
Migration Guides V4 To V5 (`docs/migration-guides/v4-to-v5.md`)
Jedis 5 Breaking Changes
All variants of
blmpopandbzmpopmethods now takedouble timeoutparameter instead oflong timeoutparameter.
This is breaking ONLY IF you are usingLongfor timeout.Reducerabstract class is refactored:Reducer(String field)constructor is removed;Reducer(String name, String field)constructor is added.Reducer(String name)constructor is added; it will cause runtime error with olderReducer(String field)constructor.getNamemethod is removed.getAliasmethod is removed.setAliasmethod is removed; useasmethod.setAliasAsFieldmethod is removed.getOwnArgsmethod is now abstract.getArgsmethod is removed.
quit()method has been removed fromConnectionandServerCommandsinterface and implementations.updatePassword(String password)method has been removed fromJedisClientConfigand implementations.setPassword(String password)method has been removed from bothJedisFactoryandConnectionFactoryclasses.Both
bzpopmax(double timeout, String... keys)andbzpopmin(double timeout, String... keys)now returnKeyValue<String, Tuple>(instead ofKeyedZSetElement).Both
bzpopmax(double timeout, byte[]... keys)andbzpopmin(double timeout, byte[]... keys)now returnKeyValue(instead ofList).Following methods now return
KeyValue<String, String>instead ofKeyedListElement:blpop(double timeout, String key)blpop(double timeout, String... keys)brpop(double timeout, String key)brpop(double timeout, String... keys)
Following methods now return
KeyValueinstead ofList:blpop(double timeout, byte[]... keys)brpop(double timeout, byte[]... keys)
zdiff(String... keys)method now returnsList<String>(instead ofSet<String>).zdiff(byte[]... keys)method now returnsList(instead ofSet).Both
zdiffWithScores(String... keys)andzdiffWithScores(byte[]... keys)methods now returnList<Tuple>(instead ofSet<Tuple>).zinter(ZParams params, String... keys)method now returnsList<String>(instead ofSet<String>).zinter(ZParams params, byte[]... keys)method now returnsList(instead ofSet).Both
zinterWithScores(ZParams params, String... keys)andzinterWithScores(ZParams params, byte[]... keys)methods now returnList<Tuple>(instead ofSet<Tuple>).zunion(ZParams params, String... keys)method now returnsList<String>(instead ofSet<String>).zunion(ZParams params, byte[]... keys)method now returnsList(instead ofSet).Both
zunionWithScores(ZParams params, String... keys)andzunionWithScores(ZParams params, byte[]... keys)methods now returnList<Tuple>(instead ofSet<Tuple>).Both
configGet(String pattern)andconfigGet(String... patterns)methods now returnMap<String, String>instead ofList<String>.Both
configGet(byte[] pattern)andconfigGet(byte[]... patterns)methods now returnMapinstead ofList.New
aclDelUser(String... names)method replacesaclDelUser(String name)andaclDelUser(String name, String... names)methods.New
aclDelUser(byte[]... names)method replacesaclDelUser(byte[] name)andaclDelUser(byte[] name, byte[]... names)methods.tsMGet(TSMGetParams multiGetParams, String... filters)method now returnsMap<String, TSMGetElement>instead ofList<TSKeyValue<TSElement>>.Following methods now return
Map<String, TSMRangeElements>instead ofList<TSKeyedElements>:tsMRange(long fromTimestamp, long toTimestamp, String... filters)tsMRange(TSMRangeParams multiRangeParams)tsMRevRange(long fromTimestamp, long toTimestamp, String... filters)tsMRevRange(TSMRangeParams multiRangeParams)
jsonNumIncrBy(String key, Path2 path, double value)method now returnsObjectinstead ofJSONArray.- The returning object would still be JSONArray for all previous cases. So simple type casting is enough to handle this change.
- The returning object will be
List<Double>when running under RESP3 protocol.
getAgeSeconds()inAccessControlLogEntrynow returnsDoubleinstead ofString.Both
ftConfigGet(String option)andftConfigGet(String indexName, String option)methods now returnMap<String, Object>instead ofMap<String, String>.ftList()method now returnsSet<String>instead ofList<String>.graphSlowlog(String graphName)now returnsList<List<Object>>(instead ofList<List<String>>).CommandListFilterByParamsnow throwsIllegalArgumentException(instead ofJedisDataException) in case of unfulfilling filter.FailoverParamsnow throwsIllegalArgumentException(instead ofIllegalStateException) in case of unfulfilling optional arguments.XPendingParamsnow throwsIllegalArgumentException(instead ofIllegalStateException) in case of unfulfilling optional arguments.get()option has been removed fromSetParams. Following methods have been added in Jedis/UnifiedJedis for convenience:setGet(String key, String value)method has been added inStringCommandsinterface.setGet(byte[] key, byte[] value)method has been added inStringBinaryCommandsinterface.
xpending(String key, String groupName, StreamEntryID start, StreamEntryID end, int count, String consumerName)method has been removed from everywhere.- Use
xpending(java.lang.String, java.lang.String, redis.clients.jedis.params.XPendingParams)instead.
- Use
xpending(byte[] key, byte[] groupName, byte[] start, byte[] end, int count, byte[] consumerName)method has been removed from everywhere.- Use
xpending(byte[], byte[], redis.clients.jedis.params.XPendingParams)instead.
- Use
retentionTime(long retentionTime)method inTSAlterParamshas been removed. Useretention(long)method instead.Following classes have been removed:
KeyedZSetElementKeyedListElementTSKeyValueTSKeyedElementsLimit
Following BuilderFactory implementations have been removed:
BYTE_ARRAY(useBINARY)BYTE_ARRAY_LIST(useBINARY_LIST)BINARY_MAP_FROM_PAIRSSTRING_ORDERED_SET
All payload related parameters are removed from search related classes; namely
Document,IndexDefinition,Query.topkCount(String key, String... items)method has been removed from everywhere.Following methods supporting JSON.RESP command have been removed:
jsonResp(String key)jsonResp(String key, Path path)jsonResp(String key, Path2 path)
RedisJsonCommandsandRedisJsonPipelineCommandsinterfaces have been moved intoredis.clients.jedis.json.commandspackage.AbortedTransactionExceptionis removed.Queableclass is removed.Paramsabstract class is removed.toString()support used by its sub-classes is now unavailable.
getParams()method is removed fromSortingParamsclass.Both
SEARCH_AGGREGATION_RESULTandSEARCH_AGGREGATION_RESULT_WITH_CURSORimplementations fromSearchBuilderFactoryclass have been moved toAggregationResultclass.All
AggregationResultconstructors have been madeprivate.getArgs(),getArgsString()andserializeRedisArgs(List redisArgs)methods have been removed fromAggregationBuilder.totalResultsvariable inAggregationResulthas been made private. UsegetTotalResults()method instead.getArgs()andlimit(Limit limit)methods have been removed fromGroupclass.addCommandEncodedArgumentsandaddCommandBinaryArgumentsmethods have been removed fromFieldNameclass.addObjects(int[] ints)method has been removed fromCommandArguments.Following methods have been removed:
strAlgoLCSStrings(String strA, String strB, StrAlgoLCSParams params)strAlgoLCSStrings(byte[] strA, byte[] strB, StrAlgoLCSParams params)strAlgoLCSKeys(String keyA, String keyB, StrAlgoLCSParams params)strAlgoLCSKeys(byte[] keyA, byte[] keyB, StrAlgoLCSParams params)
StrAlgoLCSParamsclass has been removed.Following methods have been removed from all Pipeline classes:
ftCursorRead(String indexName, long cursorId, int count)ftCursorDel(String indexName, long cursorId)ftDropIndex(String indexName)ftDropIndexDD(String indexName)ftAliasAdd(String aliasName, String indexName)ftAliasUpdate(String aliasName, String indexName)ftAliasDel(String aliasName)
JedisSentineled(String masterName, Set<HostAndPort> sentinels, JedisClientConfig masterClientConfig, JedisClientConfig sentinelClientConfig)andJedisSentineled(String masterName, Set<HostAndPort> sentinels, GenericObjectPoolConfig<Connection> poolConfig, JedisClientConfig masterClientConfig, JedisClientConfig sentinelClientConfig)
constructors have been removed.JedisClusterInfoCache(JedisClientConfig clientConfig)andJedisClusterInfoCache(JedisClientConfig clientConfig, GenericObjectPoolConfig<Connection> poolConfig)
constructors have been removed.
Migration Guides V5 To V6 (`docs/migration-guides/v5-to-v6.md`)
Jedis 6.0.0 Migration Guide
This guide helps you migrate from Jedis 5.x to Jedis 6.0.0. Version 6.0.0 includes breaking changes focused on Redis 8.0 support, removal of deprecated modules, and improvements to the Search API.
Table of Contents
Overview
Jedis 6.0.0 is a major release that adds Redis 8.0 support and removes deprecated features. The main focus areas are:
- Redis 8.0 compatibility - Full support for Redis 8.0 features including built-in JSON, Search, and TimeSeries
- Module deprecations - Removal of RedisGraph and Triggers & Functions (RedisGears v2) support
- Search API improvements - Default dialect change and enhanced profiling responses
- Security enhancements - New SSL options and token-based authentication support
Breaking Changes
Removed RedisGraph Support
RedisGraph module support has been completely removed from Jedis 6.0.0 as the module has been deprecated by Redis.
Removed Classes and Interfaces
All classes in the redis.clients.jedis.graph package have been removed:
RedisGraphCommandsinterfaceRedisGraphPipelineCommandsinterfaceGraphCommandObjectsclassGraphCache,GraphProtocol,GraphQueryParamsclassesResultSet,ResultSetBuilder,Record,Header,Statisticsclasses- All entity classes:
Edge,Node,Path,Point,Property,GraphEntity
Removed Triggers and Functions (RedisGears v2)
Support for Triggers and Functions (RedisGears v2) has been removed from Jedis 6.0.0.
Removed Classes and Interfaces
All classes in the redis.clients.jedis.gears package have been removed:
RedisGearsCommandsinterfaceRedisGearsProtocolclassTFunctionListParams,TFunctionLoadParamsclasses- Response classes:
FunctionInfo,FunctionStreamInfo,GearsLibraryInfo,StreamTriggerInfo,TriggerInfo
Search Dialect Default Change
BREAKING: The default search dialect has changed from server-side default to DIALECT 2 (client-side override).
Impact
Starting with Jedis 6.0.0, all FT.SEARCH and FT.AGGREGATE commands automatically append DIALECT 2 unless explicitly configured otherwise. This may affect query results if you were relying on DIALECT 1 behavior.
Migration Path
Option 1: Accept DIALECT 2 (Recommended)
Review your search queries to ensure they work correctly with DIALECT 2. Most queries should work without changes.
Option 2: Revert to DIALECT 1
If you need to maintain DIALECT 1 behavior:
JedisPooled jedis = new JedisPooled("redis://localhost:6379");
// Set default dialect to 1
jedis.setDefaultSearchDialect(1);
// Now all search commands will use DIALECT 1
SearchResult result = jedis.ftSearch("idx:products", "@category:electronics");FT.PROFILE Return Type Change
The return type of FT.PROFILE commands has changed from Map<String, Object> to a structured ProfilingInfo object.
Changed Methods
Before (v5.x):
Map.Entry<SearchResult, Map<String, Object>> ftProfileSearch(
String indexName, FTProfileParams profileParams, Query query);
Map.Entry<AggregationResult, Map<String, Object>> ftProfileAggregate(
String indexName, FTProfileParams profileParams, AggregationBuilder aggr);After (v6.0.0):
Map.Entry<SearchResult, ProfilingInfo> ftProfileSearch(
String indexName, FTProfileParams profileParams, Query query);
Map.Entry<AggregationResult, ProfilingInfo> ftProfileAggregate(
String indexName, FTProfileParams profileParams, AggregationBuilder aggr);COMMAND INFO Response Changes
The response format for COMMAND INFO has been updated to include subcommand details, making it compatible with Redis 7.0+ and Redis 8.0.
Impact
If you were parsing the COMMAND INFO response, you may need to update your code to handle the new structure that includes subcommand information.
Before (v5.x):
List<Object> commandInfo = jedis.commandInfo("SET");
// Returns basic command informationAfter (v6.0.0):
List<Object> commandInfo = jedis.commandInfo("SET");
// Returns command information including subcommand details
// Compatible with Redis 7.0+ formatNew Features
Redis 8.0 Support
Jedis 6.0.0 adds full support for Redis 8.0, which includes built-in support for:
- JSON - Native JSON data type (previously RedisJSON module)
- Search and Query - Full-text search capabilities (previously RediSearch module)
- TimeSeries - Time-series data support (previously RedisTimeSeries module)
Example:
JedisPooled jedis = new JedisPooled("redis://localhost:6379");
// JSON operations (built-in in Redis 8.0)
jedis.jsonSet("user:1", Path2.of("$"), "{\"name\":\"John\",\"age\":30}");
String json = jedis.jsonGet("user:1");
// Search operations (built-in in Redis 8.0)
jedis.ftCreate("idx:users",
FTCreateParams.createParams()
.on(IndexDataType.JSON)
.addPrefix("user:"),
TextField.of("$.name").as("name"),
NumericField.of("$.age").as("age"));
SearchResult result = jedis.ftSearch("idx:users", "@name:John");SslOptions for Advanced SSL Configuration
A new SslOptions class provides advanced SSL/TLS configuration options for secure connections.
Features:
- Custom keystore and truststore configuration
- SSL protocol selection
- SSL verification mode control
- SSL parameters customization
Example:
SslOptions sslOptions = SslOptions.builder()
.keystore(new File("/path/to/keystore.jks"))
.keystorePassword("keystorePassword".toCharArray())
.truststore(new File("/path/to/truststore.jks"))
.truststorePassword("truststorePassword".toCharArray())
.sslVerifyMode(SslVerifyMode.FULL)
.build();
JedisClientConfig config = DefaultJedisClientConfig.builder()
.ssl(true)
.sslOptions(sslOptions)
.build();
JedisPooled jedis = new JedisPooled("localhost", 6379, config);Token-Based Authentication
Jedis 6.0.0 introduces support for token-based authentication, useful for cloud environments and managed Redis services.
Example:
// Token-based authentication with automatic token refresh
TokenCredentials tokenCredentials = new TokenCredentials("initial-token");
JedisClientConfig config = DefaultJedisClientConfig.builder()
.credentials(tokenCredentials)
.build();
JedisPooled jedis = new JedisPooled("localhost", 6379, config);
// Token can be updated dynamically
tokenCredentials.updateToken("new-token");New Hash Commands
Support for new hash field expiration commands introduced in Redis 7.4:
HGETDEL- Get and delete a hash fieldHGETEX- Get a hash field with expiration optionsHSETEX- Set a hash field with expiration
Example:
// HSETEX - Set field with expiration
jedis.hsetex("user:1", 3600, "session", "abc123");
// HGETEX - Get field and update expiration
String session = jedis.hgetex("user:1", "session",
HGetExParams.hgetExParams().ex(7200));
// HGETDEL - Get and delete field
String oldSession = jedis.hgetdel("user:1", "session");Search Warning Messages
Search and aggregation queries now support warning messages in results, helping identify potential issues with queries.
Example:
SearchResult result = jedis.ftSearch("idx:products", "@name:laptop");
// Check for warnings
if (result.hasWarnings()) {
List<String> warnings = result.getWarnings();
warnings.forEach(warning ->
System.out.println("Search warning: " + warning));
}Additional Resources
Getting Help
If you encounter issues during migration:
- Check the Jedis GitHub Issues
- Join the Redis Discord
- Review the Jedis Javadocs
- Start a Discussion
Migration Guides V6 To V7 (`docs/migration-guides/v6-to-v7.md`)
Jedis 7.0.0 Migration Guide
This guide helps you migrate from Jedis 6.2.0 to Jedis 7.0.0. Version 7.0.0 includes several breaking changes focused on removing deprecated features and improving the API design.
Table of Contents
Overview
Jedis 7.0.0 is a major release that removes previously deprecated features and modernizes the API. The main focus areas are:
- Removal of deprecated sharding features - JedisSharding and related classes have been removed
- Base class consolidation - Pipeline and Transaction base classes have been renamed
- Builder pattern introduction - New fluent builders for JedisPooled, JedisCluster, and JedisSentineled
- API cleanup - Removal of deprecated constructors and methods
Breaking Changes
Removed Deprecated Sharding Features
The following classes and features have been completely removed in Jedis 7.0.0:
Removed Classes
JedisSharding- UseJedisClusterfor distributed Redis deployments insteadShardedPipeline- UsePipelinewithJedisClusterinsteadShardedConnectionProvider- No replacement neededShardedCommandArguments- Internal class, no replacement neededShardedCommandObjects- Internal class, no replacement needed
Migration Path
If you were using JedisSharding:
Before (v6.2.0):
List<HostAndPort> shards = Arrays.asList(
new HostAndPort("localhost", 6379),
new HostAndPort("localhost", 6380)
);
JedisSharding jedisSharding = new JedisSharding(shards);
jedisSharding.set("key", "value");After (v7.0.0):
// Option 1: Use JedisCluster for distributed deployments
Set<HostAndPort> nodes = new HashSet<>(Arrays.asList(
new HostAndPort("localhost", 6379),
new HostAndPort("localhost", 6380)
));
JedisCluster jedisCluster = new JedisCluster(nodes);
jedisCluster.set("key", "value");
// Option 2: Use JedisPooled for single-node deployments
JedisPooled jedis = new JedisPooled("localhost", 6379);
jedis.set("key", "value");Base Class Changes
Several base classes have been renamed to better reflect their purpose:
Pipeline Base Class
PipelineBasehas been removedPipelinenow extendsAbstractPipelineinstead
Impact: If you were using PipelineBase as a type reference, change it to AbstractPipeline.
Before (v6.2.0):
PipelineBase pipeline = jedis.pipelined();After (v7.0.0):
AbstractPipeline pipeline = jedis.pipelined();
// Or use the concrete type:
Pipeline pipeline = (Pipeline) jedis.pipelined();Transaction Base Class
TransactionBasehas been removedTransactionnow extendsAbstractTransactioninstead
Impact: If you were using TransactionBase as a type reference, change it to AbstractTransaction.
Before (v6.2.0):
TransactionBase transaction = jedis.multi();After (v7.0.0):
AbstractTransaction transaction = jedis.multi();
// Or use the concrete type:
Transaction transaction = (Transaction) jedis.multi();UnifiedJedis Constructor Changes
Several deprecated constructors have been removed from UnifiedJedis:
Removed Constructors
Cluster constructors with maxAttempts:
java// REMOVED in v7.0.0 UnifiedJedis(Set<HostAndPort> nodes, JedisClientConfig config, int maxAttempts) UnifiedJedis(Set<HostAndPort> nodes, JedisClientConfig config, int maxAttempts, Duration maxTotalRetriesDuration) UnifiedJedis(Set<HostAndPort> nodes, JedisClientConfig config, GenericObjectPoolConfig<Connection> poolConfig, int maxAttempts, Duration maxTotalRetriesDuration)Sharding constructors:
java// REMOVED in v7.0.0 UnifiedJedis(ShardedConnectionProvider provider) UnifiedJedis(ShardedConnectionProvider provider, Pattern tagPattern)
Migration Path
Before (v6.2.0):
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("localhost", 6379));
JedisClientConfig config = DefaultJedisClientConfig.builder().build();
UnifiedJedis jedis = new UnifiedJedis(nodes, config, 3);After (v7.0.0):
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("localhost", 6379));
JedisClientConfig config = DefaultJedisClientConfig.builder().build();
// Use JedisCluster instead
JedisCluster jedis = new JedisCluster(nodes, config);
// Or use the new builder pattern
JedisCluster jedis = JedisCluster.builder()
.nodes(nodes)
.clientConfig(config)
.build();Return Type Changes
UnifiedJedis.pipelined()
The return type has been changed to be more generic:
Before (v6.2.0):
PipelineBase pipelined()After (v7.0.0):
AbstractPipeline pipelined()Impact: Minimal - AbstractPipeline is the parent class, so existing code should continue to work.
New Features
Automatic Failover and Failback
Jedis 7.0.0 significantly refactors the automatic failover and failback API. If you were using the failover features in v6.2.0 with MultiClusterClientConfig and MultiClusterPooledConnectionProvider, these have been renamed and improved in v7.0.0.
For detailed migration guidance on automatic failover and failback please refer to the Automatic Failover and Failback Migration Guide.
Builder Pattern for Client Creation
Jedis 7.0.0 introduces a fluent builder pattern for creating client instances, making configuration more intuitive and discoverable.
JedisPooled Builder
New in v7.0.0:
JedisPooled jedis = JedisPooled.builder()
.hostAndPort("localhost", 6379)
.clientConfig(DefaultJedisClientConfig.builder()
.user("myuser")
.password("mypassword")
.database(0)
.build())
.poolConfig(new GenericObjectPoolConfig<>())
.build();JedisCluster Builder
New in v7.0.0:
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("localhost", 7000));
nodes.add(new HostAndPort("localhost", 7001));
JedisCluster cluster = JedisCluster.builder()
.nodes(nodes)
.clientConfig(DefaultJedisClientConfig.builder()
.password("mypassword")
.build())
.maxAttempts(5)
.maxTotalRetriesDuration(Duration.ofSeconds(10))
.build();JedisSentineled Builder
New in v7.0.0:
Set<HostAndPort> sentinels = new HashSet<>();
sentinels.add(new HostAndPort("localhost", 26379));
sentinels.add(new HostAndPort("localhost", 26380));
JedisSentineled jedis = JedisSentineled.builder()
.masterName("mymaster")
.sentinels(sentinels)
.clientConfig(DefaultJedisClientConfig.builder()
.password("mypassword")
.build())
.build();Getting Help
If you encounter issues during migration create an issue or start a discussion.
Migration Guides V7 To V8 (`docs/migration-guides/v7-to-v8.md`)
Jedis 8.0.0 Migration Guide
This guide helps you migrate from Jedis 7.5.0 to Jedis 8.0.0. Version 8.0.0 introduces several breaking changes focused on completing the client class consolidation, modernizing protocol defaults, and tightening security defaults.
Table of Contents
- Overview
- Breaking Changes
- RESP3 Negotiated by Default
- TLS Hostname Verification Enforced by Default
- Removed Client Classes (
JedisPooled,JedisSentineled) - Removed
UnifiedJedisPublic Constructors Transaction(Jedis)andClusterPipeline(ClusterConnectionProvider)Constructors RemovedCommandObjectsProtocol is Now ImmutablebroadcastCommandandJedisBroadcastAndRoundRobinConfigRemoved- Removed Sharding Utility Classes
aclLogBinary()Removedcommons-pool2Upgraded to 2.13.1
- Deprecations
- New Features
Overview
Jedis 8.0.0 is a major release that finishes the client-class consolidation started in 7.x and modernizes a few long-standing defaults. The main focus areas are:
- RESP3 auto-negotiation on by default for all
UnifiedJedis-based clients (with graceful RESP2 fallback) - Final removal of legacy
JedisPooled/JedisSentineledclasses in favor of theRedisClientfamily introduced in 7.0.0 - Removal of deprecated
UnifiedJedispublic constructors — clients are now created exclusively through the dedicated client classes or their builders - Internal refactoring that affects users of low-level extension points (custom
CommandExecutor,Transactionsubclasses,CommandObjects)
Breaking Changes
RESP3 Negotiated by Default
UnifiedJedis-based clients (RedisClient, RedisClusterClient, RedisSentinelClient, MultiDbClient, and the deprecated JedisCluster) now auto-negotiate the RESP3 protocol by default.
The negotiation logic:
- When
JedisClientConfig.getRedisProtocol() == nullandisAutoNegotiateProtocol() == true(the new default), the client sendsHELLO 3on connect and gracefully falls back to RESP2 if the server rejects it. - When
getRedisProtocol()isRESP2orRESP3, that version is enforced strictly — the connection fails if the server rejects it. - When
getRedisProtocol() == nullandisAutoNegotiateProtocol() == false, the legacy "no HELLO" behavior is preserved (RESP2 assumed on the wire).
The legacy Jedis class doesn't fully support RESP3 protocol format and therefore ignores the auto-negotiation flag; it emits a warning if it is left enabled. To suppress the warning in Jedis-based code paths, use the new serverDefaultProtocol() shortcut:
JedisClientConfig config = DefaultJedisClientConfig.builder()
.serverDefaultProtocol() // equivalent to .protocol(null).autoNegotiateProtocol(false)
.build();
Jedis jedis = new Jedis(host, port, config);New `JedisClientConfig` API
public interface JedisClientConfig {
// ...existing...
default boolean isAutoNegotiateProtocol() { return true; } // NEW, default = true
}// New builder methods on DefaultJedisClientConfig.Builder
Builder resp2(); // shortcut for protocol(RESP2)
Builder serverDefaultProtocol(); // legacy "no HELLO" mode
Builder autoNegotiateProtocol(boolean value);If your application relied on RESP2 reply shapes for commands whose RESP3 shape differs (e.g. HRANDFIELD WITHVALUES, JSON.TYPE, XREAD/XREADGROUP, certain RediSearch shapes), pin the protocol explicitly:
JedisClientConfig config = DefaultJedisClientConfig.builder().resp2().build();
RedisClient client = RedisClient.builder().clientConfig(config).build();Other RESP3-related fixes folded into this change
JSON.NUMINCRBYnow correctly returns the original numeric type (long or double) on RESP3 instead of always returning a double.- RediSearch
NOCONTENTresponses now return an empty map on RESP3 instead of throwing on field access; missing scores default to0.0and missing warnings default to an empty list on both protocols. - The deprecated
setProtocol(RedisProtocol)mutator onJediswas removed as part of the same change set.
TLS Hostname Verification Enforced by Default
DefaultJedisSocketFactory now sets SSLParameters.setEndpointIdentificationAlgorithm("HTTPS") whenever SSL is enabled and the user has not supplied custom SSLParameters. Connections to a TLS endpoint whose certificate CN/SAN does not match the host you connect to will now fail with an SSL handshake error.
This is a security-positive change but may surface previously silent misconfigurations: self-signed certificates without a matching SAN, hostname/IP mismatches between client and certificate, or wildcard-SAN limits will now reject the connection instead of silently accepting it.
Migration Path
The recommended migration is to fix the certificate (add a SAN that matches the connection host) or to use the modern SslOptions API, which exposes a clear SslVerifyMode:
SslOptions sslOptions = SslOptions.builder()
.truststore(trustStore)
.sslVerifyMode(SslVerifyMode.FULL) // hostname verification on
.build();
JedisClientConfig config = DefaultJedisClientConfig.builder()
.sslOptions(sslOptions)
.build();If you intentionally need to bypass hostname verification (not recommended for production), supply explicit SSLParameters with no endpoint identification algorithm:
SSLParameters sslParameters = new SSLParameters();
// leave endpointIdentificationAlgorithm null/empty to disable verification
JedisClientConfig config = DefaultJedisClientConfig.builder()
.ssl(true)
.sslParameters(sslParameters)
.build();Note: the legacy
ssl(),sslSocketFactory(),sslParameters(), andhostnameVerifier()setters used in the second snippet are themselves deprecated — see Legacy SSL Configuration Setters Deprecated.
Removed Client Classes (`JedisPooled`, `JedisSentineled`)
JedisPooled and JedisSentineled were deprecated in 7.0.0 and have now been completely removed. Use the corresponding classes from the RedisClient family introduced in 7.0.0.
| v7.5.0 | v8.0.0 |
|---|---|
redis.clients.jedis.JedisPooled |
redis.clients.jedis.RedisClient |
redis.clients.jedis.JedisSentineled |
redis.clients.jedis.RedisSentinelClient |
Migration Path
Before (v7.5.0):
// Standalone
JedisPooled jedis = new JedisPooled("localhost", 6379);
jedis.set("key", "value");
// Sentinel
JedisSentineled sentinel = new JedisSentineled(
"mymaster", masterConfig, sentinels, sentinelConfig);After (v8.0.0):
// Standalone — simple factory
RedisClient jedis = RedisClient.create("localhost", 6379);
jedis.set("key", "value");
// Standalone — builder for advanced configuration
RedisClient jedis = RedisClient.builder()
.hostAndPort("localhost", 6379)
.clientConfig(DefaultJedisClientConfig.builder()
.user("myuser")
.password("mypassword")
.build())
.build();
// Sentinel — builder
RedisSentinelClient sentinel = RedisSentinelClient.builder()
.masterName("mymaster")
.sentinels(sentinels)
.clientConfig(masterConfig)
.sentinelClientConfig(sentinelConfig)
.build();Removed `UnifiedJedis` Public Constructors
All previously deprecated public constructors on UnifiedJedis have been removed. The class is no longer intended to be instantiated directly by users — use RedisClient, RedisClusterClient, RedisSentinelClient, or MultiDbClient (and their builders) instead.
Removed constructors
// REMOVED in v8.0.0
UnifiedJedis()
UnifiedJedis(HostAndPort hostAndPort)
UnifiedJedis(String url)
UnifiedJedis(URI uri)
UnifiedJedis(URI uri, JedisClientConfig config)
UnifiedJedis(HostAndPort hostAndPort, JedisClientConfig clientConfig)
UnifiedJedis(HostAndPort hostAndPort, JedisClientConfig clientConfig, CacheConfig cacheConfig)
UnifiedJedis(HostAndPort hostAndPort, JedisClientConfig clientConfig, Cache cache)
UnifiedJedis(ConnectionProvider provider)
UnifiedJedis(JedisSocketFactory socketFactory)
UnifiedJedis(JedisSocketFactory socketFactory, JedisClientConfig clientConfig)
UnifiedJedis(CommandExecutor executor)The following constructor changed visibility from public to protected:
// Now protected (still callable from subclasses)
UnifiedJedis(Connection connection)The following constructor is not removed, but is now @Deprecated — call sites continue to compile (the wider ConnectionProvider signature absorbs former ClusterConnectionProvider callers), but new code should use RedisClusterClient.builder():
@Deprecated
public UnifiedJedis(ConnectionProvider provider, int maxAttempts, Duration maxTotalRetriesDuration)Migration Path
Before (v7.5.0):
UnifiedJedis jedis = new UnifiedJedis(new HostAndPort("localhost", 6379));
UnifiedJedis jedis2 = new UnifiedJedis(URI.create("redis://localhost:6379"));
UnifiedJedis custom = new UnifiedJedis(myConnectionProvider);After (v8.0.0):
RedisClient jedis = RedisClient.create(new HostAndPort("localhost", 6379));
RedisClient jedis2 = RedisClient.create("redis://localhost:6379");
RedisClient custom = RedisClient.builder()
.connectionProvider(myConnectionProvider)
.build();`Transaction(Jedis)` and `ClusterPipeline(ClusterConnectionProvider)` Constructors Removed
`Transaction(Jedis)`
The legacy public Transaction(Jedis jedis) constructor — which Jedis used internally to wire resetState() callbacks — has been removed. The coupling is now handled inside Jedis.multi() via the new onAfterExec() / onAfterDiscard() hook methods.
If you subclassed Transaction or constructed it manually with a Jedis reference, switch to the Connection-based constructor and override the hooks if you need exec/discard notifications.
Before (v7.5.0):
public Transaction(Jedis jedis); // public, took a JedisAfter (v8.0.0):
public Transaction(Connection connection); // existing
public Transaction(Connection connection, boolean doMulti);
public Transaction(Connection connection, boolean doMulti, boolean closeConnection);
// New protected hooks to override:
protected void onAfterExec();
protected void onAfterDiscard();Additionally, the deprecated protected AbstractTransaction() no-arg constructor has been removed — subclasses must call AbstractTransaction(CommandObjects commandObjects).
`ClusterPipeline(ClusterConnectionProvider)`
The single-argument public ClusterPipeline(ClusterConnectionProvider provider) constructor has been removed. It silently constructed a ClusterCommandObjects with no protocol information, which is no longer valid now that CommandObjects requires an explicit protocol (see CommandObjects Protocol is Now Immutable).
A new overload accepts the protocol explicitly:
Before (v7.5.0):
new ClusterPipeline(provider);After (v8.0.0):
new ClusterPipeline(provider, RedisProtocol.RESP3); // or RESP2
// or, if you already have a ClusterCommandObjects:
new ClusterPipeline(provider, clusterCommandObjects); // unchanged`CommandObjects` Protocol is Now Immutable
The internal CommandObjects class no longer allows mutating the protocol after construction:
- The no-arg
public CommandObjects()constructor has been removed. - The
public final void setProtocol(RedisProtocol)setter has been removed. public CommandObjects(RedisProtocol protocol)now throwsIllegalArgumentExceptionifprotocolisnull— useRedisProtocol.orServerDefault(protocol)(which falls back toRESP2) when you do not know the negotiated protocol up front.
This only affects code that integrates with Jedis internals (custom UnifiedJedis subclasses, custom CommandExecutor, instrumentation). Application code that uses the RedisClient* builders is unaffected.
Before (v7.5.0):
CommandObjects commandObjects = new CommandObjects();
// ... later
commandObjects.setProtocol(RedisProtocol.RESP3);After (v8.0.0):
CommandObjects commandObjects = new CommandObjects(RedisProtocol.orServerDefault(protocol));A new constant RedisProtocol.REDIS_SERVER_DEFAULT_PROTO (= RESP2) and a helper RedisProtocol.orServerDefault(RedisProtocol) are provided for this purpose.
`broadcastCommand` and `JedisBroadcastAndRoundRobinConfig` Removed
Broadcast routing is no longer a separate API surface — it is now driven entirely by the new request/response policy registry (see Request/Response Policy Support for Cluster Commands). The following has been removed:
- The
JedisBroadcastAndRoundRobinConfiginterface (including theRediSearchMode.DEFAULT/RediSearchMode.LIGHTenum) is deleted. UnifiedJedis.setBroadcastAndRoundRobinConfig(JedisBroadcastAndRoundRobinConfig)is removed.- The default
broadcastCommand(CommandObject<T>)method on theCommandExecutorinterface is removed. UnifiedJedis.broadcastCommand(...)is removed — broadcast routing now flows throughexecuteCommand(...)and is resolved by the registry.
Migration Path
In v7.x, users running RediSearch against Redis 7 (where FT.CREATE only created the index on the contacted shard) had to opt into broadcasting indexes manually:
// v7.5.0 — manual broadcast opt-in for cluster + RediSearch
JedisBroadcastAndRoundRobinConfig cfg = () -> RediSearchMode.DEFAULT;
jedis.setBroadcastAndRoundRobinConfig(cfg);
jedis.ftCreate("idx", IndexOptions.defaultOptions(), schema);This is no longer necessary in v8.0.0. Redis 8.0+ broadcasts FT.CREATE automatically on the server side as well, so the index is created cluster-wide regardless. Simply drop the configuration:
// v8.0.0 — no special configuration required
jedis.ftCreate("idx", IndexOptions.defaultOptions(), schema);Custom CommandExecutor implementations that overrode broadcastCommand should remove the override.
Removed Sharding Utility Classes
The following utility classes — leftovers from the JedisSharding feature removed in 7.0.0 — have been deleted:
redis.clients.jedis.util.Hashingredis.clients.jedis.util.MurmurHash
If you happened to use these utility classes for application-level hashing, copy them into your own codebase or switch to an equivalent library (Guava's Hashing.murmur3_*, etc.).
`aclLogBinary()` Removed
Jedis.aclLogBinary() and aclLogBinary(int) (declared on AccessControlLogBinaryCommands) have been removed. Their declared return type List could not represent the ACL LOG reply, which is a nested array (a list of entries, each a flattened list of key/value pairs). Reading any element threw ClassCastException, so the methods were never usable.
Migration Path
Use aclLog() / aclLog(int), which return structured AccessControlLogEntry objects for the same data:
List<AccessControlLogEntry> log = jedis.aclLog();`commons-pool2` Upgraded to 2.13.1
The org.apache.commons:commons-pool2 dependency has been bumped from 2.12.1 to 2.13.1. The connection-close exception handling inside MultiDbCommandExecutor was adjusted accordingly. If you pin commons-pool2 directly in your pom.xml / build.gradle, update it to 2.13.1 to stay consistent with what Jedis is tested against.
Deprecations
`JedisCluster` Deprecation
JedisCluster is now @Deprecated in favor of RedisClusterClient. The class still works, but new code should use RedisClusterClient.
Before (v7.5.0):
JedisCluster cluster = new JedisCluster(nodes, clientConfig);After (v8.0.0):
RedisClusterClient cluster = RedisClusterClient.builder()
.nodes(nodes)
.clientConfig(clientConfig)
.build();Legacy SSL Configuration Setters Deprecated
The legacy SSL configuration surface on JedisClientConfig and DefaultJedisClientConfig.Builder is now @Deprecated (since 7.4.2) in favor of SslOptions:
// Deprecated on JedisClientConfig:
SSLSocketFactory getSslSocketFactory();
SSLParameters getSslParameters();
// (HostnameVerifier was already deprecated)
// And the corresponding builder setters:
DefaultJedisClientConfig.Builder.ssl(boolean);
DefaultJedisClientConfig.Builder.sslSocketFactory(SSLSocketFactory);
DefaultJedisClientConfig.Builder.sslParameters(SSLParameters);
DefaultJedisClientConfig.Builder.hostnameVerifier(HostnameVerifier);SslOptions is the single configuration knob going forward. It bundles trust store, key store, verify mode, and protocol selection in one place, and takes precedence over the legacy setters when both are present:
Before (v7.5.0):
JedisClientConfig config = DefaultJedisClientConfig.builder()
.ssl(true)
.sslSocketFactory(mySocketFactory)
.sslParameters(myParameters)
.hostnameVerifier(myVerifier)
.build();After (v8.0.0):
SslOptions sslOptions = SslOptions.builder()
.truststore(trustStoreFile, trustStorePassword)
.keystore(keyStoreFile, keyStorePassword)
.sslVerifyMode(SslVerifyMode.FULL)
.build();
JedisClientConfig config = DefaultJedisClientConfig.builder()
.sslOptions(sslOptions)
.build();RediSearch Cursor APIs Deprecated
The low-level cursor methods on RediSearchCommands are deprecated because they don't work reliably on cluster deployments (the cursor lives on a specific shard, but the next command may be routed elsewhere):
// Deprecated in v8.0.0
AggregationResult ftCursorRead(String indexName, long cursorId, int count);
String ftCursorDel(String indexName, long cursorId);The companion search-side iterator class is deprecated for the same reasons:
// Deprecated in v8.0.0
redis.clients.jedis.search.FtSearchIterationFor FT.SEARCH, call UnifiedJedis#ftSearch(String, String, FTSearchParams) directly and paginate via FTSearchParams.limit(offset, count). For FT.AGGREGATE, use the new ftAggregateIterator(...) method, which encapsulates cursor lifecycle and connection affinity:
Before (v7.5.0):
AggregationBuilder b = new AggregationBuilder("*").cursor(100);
AggregationResult page = jedis.ftAggregate(idx, b);
while (page.getCursorId() != 0) {
page = jedis.ftCursorRead(idx, page.getCursorId(), 100);
}After (v8.0.0):
AggregationBuilder b = new AggregationBuilder("*").cursor(100);
try (AggregateIterator it = jedis.ftAggregateIterator(idx, b)) {
while (it.hasNext()) {
AggregationResult page = it.next();
// process page
}
}AggregationResult also gains an isEmpty() convenience method.
New Features
Request/Response Policy Support for Cluster Commands
Jedis 8.0.0 introduces a comprehensive request/response policy framework that aligns with the Redis command tips — every command now declares how it should be routed (single shard, all shards, all masters, keyless) and how multi-shard responses should be aggregated (logical-AND, sum, set-union, etc.).
Highlights:
- New
CommandFlagsRegistryinterface — every command now exposes its request/response policy metadata. CommandArguments.isKeyless()to inspect routing intent.- New exceptions:
ClusterAggregationException,UnsupportedAggregationException.
For most users this is transparent — cluster commands that previously needed manual fan-out (INFO, CONFIG SET, DBSIZE, etc.) now work correctly out of the box on RedisClusterClient.
Out-of-Band Push Notification Handling
Connection and Protocol now understand out-of-band RESP3 push messages and route them through a configurable PushConsumerChain. Unsupported push types are silently dropped instead of failing the connection. This makes it safe to enable features such as CLIENT TRACKING on regular connections — invalidation pushes are no longer treated as protocol errors.
New (experimental) APIs:
@Experimental public interface PushConsumer {
PushConsumerContext handle(PushConsumerContext context);
}
@Experimental public interface PushConsumerChain { /* ... */ }
@Experimental public final class PushMessage { /* ... */ }
@Experimental public final class PushMessageTypes { /* ... */ }Cursor-Based `FT.AGGREGATE` Iterator
See RediSearch Cursor APIs Deprecated. The new AggregateIterator (returned by ftAggregateIterator(...)) is cluster-safe and implements AutoCloseable, so it can be used in a try-with-resources block.
`CONFIG GET`/`SET` on `UnifiedJedis`
UnifiedJedis (and therefore all RedisClient* clients) now exposes:
public Map<String, String> configGet(String pattern);
public Map<String, String> configGet(String... patterns);
public String configSet(Map<String, String> parameterValues);On RedisClusterClient, configGet is routed to a random node (configuration is assumed to be uniform across the cluster), while configSet fans out to all masters so the change is applied cluster-wide (see the request/response policy framework).
Shared Executor for `ClusterPipeline`
JedisCluster.pipelined() / RedisClusterClient.pipelined() previously created and tore down a private ExecutorService for every pipeline, which was wasteful for short pipelines under load. A new overload accepts a user-supplied executor:
public ClusterPipeline pipelined(ExecutorService executorService);Pass a shared executor when you create many short pipelines; reuse semantics are documented on the method.
Pool Access on `RedisSentinelClient`
RedisSentinelClient now exposes the underlying primary-node connection pools (issue #4469):
public Map<?, Pool<Connection>> getPrimaryNodesConnectionMap();The returned map is keyed by master HostAndPort and yields the Pool<Connection> backing each primary, allowing instrumentation, draining, and pool-size monitoring without subclassing the provider.
Getting Help
If you encounter issues during migration create an issue or start a discussion.
Release Notes 8.1.0 (`docs/release-notes/8.1.0.md`)
Jedis 8.1.0 Release Notes
This guide helps you upgrade from Jedis 8.0.0.
!!! warning "Unreleased"
Jedis 8.1.0 has not been released yet. These notes track changes on master
since 8.0.0 and are updated as changes merge; remove this notice when 8.1.0
ships.
!!! note
If your change alters user-visible behavior, add an entry here in the same PR.
Highlights
Smart Client Handoffs ([#4668](https://github.com/redis/jedis/pull/4668))
Jedis now reacts to server maintenance push notifications with relaxed timeouts and pre-handoffs.
The feature requires RESP3 and is configured with MaintenanceNotificationsConfig viaRedisClient.builder().maintenanceNotifications(...). It is enabled by default (Mode.AUTO) on
RESP3 connections; pass MaintenanceNotificationsConfig.DISABLED to opt out. See the
Smart Client Handoffs guide.
Capacity hints for collection-built commands ([#4737](https://github.com/redis/jedis/pull/4737))
CommandArguments gained ensureCapacity(int), a chainable hint that pre-sizes the backing
argument list for the expected total number of arguments (command token included — the RESP
array length of the final command):
CommandArguments args = new CommandArguments(ZADD)
.ensureCapacity(2 + 2 * members.size()) // command + key + member/score pairs
.key(key);This avoids repeated ArrayList growth copies when large commands are built directly, e.g.
via executeCommand. Under-estimating falls back to normal growth, so correctness never
depends on the estimate. The typed API benefits automatically: collection-built commands
(MSET, map HSET/HMSET, ZADD, GEOADD, XADD, addObjects(...)) now pre-size
internally from the known collection size.
API Additions
Vector set plain overloads ([#4693](https://github.com/redis/jedis/pull/4693))
Parameter-free overloads for the experimental vector-set methods that previously required null
params: vadd/vaddFP32 with reduceDim, and the vsim*WithScores and vsim*WithScoresAndAttribs
variants, across the String, binary, and pipeline APIs.
Behavior Changes
Changes that may affect existing applications even though they are
not breaking API changes.
Closing a client no longer stops its `AuthXManager`
An AuthXManager is created by the application and passed in through JedisClientConfig, so its
lifecycle now belongs to the application, like the config itself. Previously ConnectionPool.close()
stopped the manager unconditionally, which broke every other user of a shared manager: after a
Sentinel master failover the new master pool was left without token renewal, and closing one of
several clients built on the same manager stopped renewal for the rest. Cluster and multi-db clients,
on the other hand, never stopped it. All clients now behave the same and leave the manager running.
Closed and destroyed pools are also properly unregistered from the manager. Previously the
unregistration silently failed, so a stale pool kept receiving post-authentication callbacks and
made the token manager's renewal cycle fail on its closed pool.
Action required: call authXManager.stop() after closing the last client that uses it.
Applications that relied on RedisClient.close(), JedisPooled.close() orRedisSentinelClient.close() to stop the manager will otherwise keep its renewal threads alive.
Key pre-processor applied to all key arguments ([#4675](https://github.com/redis/jedis/pull/4675))
The source key of ZRANGESTORE/GEOSEARCHSTORE and the script KEYS passed toeval(script, keyCount, ...) / evalsha(sha1, keyCount, ...) were previously not
treated as keys: a configured CommandKeyArgumentPreProcessor (e.g. key prefixing)
did not transform them, and the store commands' source key was invisible to cluster
key routing. They are now handled like every other key argument: the pre-processor
applies to them, and on cluster a cross-slot ZRANGESTORE/GEOSEARCHSTORE fails
client-side with JedisClusterOperationException instead of the server's CROSSSLOT
error.
Action required: remove any manual prefixing workaround for these arguments
(they would now be prefixed twice), and update catch blocks that matched the
server CROSSSLOT JedisDataException for these commands.
Pub/sub push messages are no longer delivered as command replies outside a subscription ([#4701](https://github.com/redis/jedis/pull/4701))
On RESP3, a pub/sub push frame that arrived while no pub/sub read loop was running —
for example a message the server delivered after the UNSUBSCRIBE confirmation,
as can happen behind the Redis Enterprise proxy — was returned as the next command's
reply. This desynced the reply stream and typically surfaced asClassCastException: java.util.ArrayList cannot be cast to [B.
Such stray pub/sub pushes are now logged at debug level and dropped instead of being
delivered to the application, and the following command receives its own reply.
Behavior during an active subscribe/psubscribe/ssubscribe loop is unchanged.
Custom PushConsumers registered on a connection now also observe pub/sub-typed
pushes received outside a subscription, which the built-in consumer previously
propagated before they could see them; the public PUBSUB_CONSUMER constant is
unchanged and still propagates unconditionally.
Action required: none for typical applications. Code that relied on reading raw
pub/sub frames from a connection without going through the JedisPubSub/JedisShardedPubSub loops (e.g. hand-rolled sendCommand(SUBSCRIBE) +getUnflushedObject()) will no longer receive those frames; use the pub/sub APIs or
register a custom PushConsumer that propagates them.
--- METRICS ---
- Files Extracted: 27
- Estimated Token Budget: ~59181 tokens
- Canonical Reference: https://codewiki.google/github.com/redis/jedis