Client-side geographic failover
Configure resilient, failover-aware Redis connections in a Spring Data Redis application.
Client-side geographic failover improves the availability of your connections to Redis: if the endpoint your application is using fails or becomes too slow, the client automatically switches to another healthy endpoint, and then fails back when the preferred endpoint recovers. See Client-side geographic failover for an overview of the concepts, including failover, failback, and health checks.
Spring Data Redis does not yet expose this feature through its own configuration.
However, Spring Data Redis works with both the Lettuce
and Jedis clients, and both
provide a MultiDbClient failover API. You can therefore use failover as a workaround:
configure a failover-aware client as a Spring bean, then use it throughout your
application. This page shows how to do this with either client. Select the tab for
the client you use.
Requirements
- Spring Data Redis 4.1.x or later.
- Two or more Redis endpoints that can serve your application. These can be two standalone Redis servers or two Active-Active database endpoints in different locations.
- To use failover with Lettuce, ensure your project uses a Lettuce version that provides the failover API (
io.lettuce.core.failover). Spring Data Redis pulls in Lettuce transitively, so you may need to override the managed Lettuce version to one that includes it. - To use failover with Jedis, use Jedis 7 or later (which provides
MultiDbClient) and add theresilience4jdependencies. See the Jedis client-side geographic failover page for the full dependency list.
Configure a failover-aware client
Define a Spring configuration class
that creates a MultiDbClient with one entry for each endpoint and exposes it as a
bean. The weight orders the endpoints: the client tries the highest-weighted
healthy endpoint first and uses lower-weighted endpoints only while the preferred
one is unhealthy.
import io.lettuce.core.RedisURI;
import io.lettuce.core.failover.MultiDbClient;
import io.lettuce.core.failover.api.DatabaseConfig;
import io.lettuce.core.failover.api.InitializationPolicy;
import io.lettuce.core.failover.api.MultiDbOptions;
import io.lettuce.core.failover.api.StatefulRedisMultiDbConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class MultiDbCacheConfig {
// The higher weight is tried first; the lower-weighted node is used
// only while the primary is unhealthy.
private static final String PRIMARY_URI = "redis://localhost:6379";
private static final float PRIMARY_WEIGHT = 1.0f;
private static final String SECONDARY_URI = "redis://localhost:6380";
private static final float SECONDARY_WEIGHT = 0.5f;
@Bean(destroyMethod = "shutdown")
public MultiDbClient failoverClient() {
// No healthCheckStrategySupplier() is set, so the default PingStrategy
// is used: a periodic PING with the library defaults.
DatabaseConfig primary = DatabaseConfig.builder(RedisURI.create(PRIMARY_URI)).weight(PRIMARY_WEIGHT).build();
DatabaseConfig secondary = DatabaseConfig.builder(RedisURI.create(SECONDARY_URI)).weight(SECONDARY_WEIGHT).build();
// ONE_AVAILABLE lets the client start as long as at least one node is
// up. Failback to the higher-weighted node is enabled by default.
MultiDbOptions options = MultiDbOptions.builder()
.initializationPolicy(InitializationPolicy.BuiltIn.ONE_AVAILABLE)
.build();
MultiDbClient client = MultiDbClient.create(List.of(primary, secondary), options);
return client;
}
@Bean(destroyMethod = "close")
public StatefulRedisMultiDbConnection<String, String> failoverConnection(MultiDbClient failoverClient) {
// Same surface as a normal StatefulRedisConnection (sync/async/reactive),
// but routing and failover are handled inside the connection.
return failoverClient.connect();
}
}
With Lettuce, the MultiDbClient produces a StatefulRedisMultiDbConnection, which
offers the same synchronous, asynchronous, and reactive APIs as an ordinary Lettuce
connection.
The examples above hard-code the endpoints and weights for clarity. In a real
application, you would typically supply them from an external property source,
such as application.yaml, using
Spring Boot configuration properties.
Use the client
Inject the failover-aware bean wherever you need it. Your code does not need to know about the failover behavior: every command runs against the endpoint that the client currently considers healthy, so if the active endpoint fails, the next command is served by another endpoint without any change to your code.
import io.lettuce.core.SetArgs;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.failover.api.StatefulRedisMultiDbConnection;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CacheController {
private final RedisCommands<String, String> redis;
public CacheController(StatefulRedisMultiDbConnection<String, String> connection) {
// .sync() is the same API as on a plain StatefulRedisConnection;
// the multi-db connection just transparently routes the command.
this.redis = connection.sync();
}
@GetMapping("/api/cache/{key}")
public String get(@PathVariable String key) {
String value = redis.get(key); // runs against the active endpoint
if (value == null) {
value = expensiveLookup(key); // compute the value on a miss
redis.set(key, value, SetArgs.Builder.ex(60)); // SET with a 60s TTL
}
return value;
}
private static String expensiveLookup(String key) {
// ... your application logic ...
return "value-for-" + key;
}
}
This approach wires the failover-aware client (or its connection) directly as a bean,
rather than through Spring Data Redis's RedisConnectionFactory and RedisTemplate.
Any component that issues commands through this bean is routed to the currently healthy
endpoint. Note that, by default, a command already in flight when a failover occurs may
fail rather than being retried on the new endpoint; see the client-specific pages below
for the retry options.
Observe failover
To make failover transitions visible, register a listener that runs on each switch.
Subscribe to Lettuce's event bus when you create the client. The DatabaseSwitchEvent
fires each time the client switches endpoint, and the AllDatabasesUnhealthyEvent
fires when no endpoint is available. The example below logs these events, so declare
an SLF4J logger field on the configuration class:
private static final Logger log = LoggerFactory.getLogger(MultiDbCacheConfig.class);
Then subscribe to the events in the failoverClient() bean method, before returning
the client:
import io.lettuce.core.failover.event.AllDatabasesUnhealthyEvent;
import io.lettuce.core.failover.event.DatabaseSwitchEvent;
client.getResources().eventBus().get()
.filter(e -> e instanceof DatabaseSwitchEvent)
.cast(DatabaseSwitchEvent.class)
.subscribe(e -> log.warn("Redis failover: {} -> {} (reason={})",
e.getFromDb(), e.getToDb(), e.getReason()));
client.getResources().eventBus().get()
.filter(e -> e instanceof AllDatabasesUnhealthyEvent)
.cast(AllDatabasesUnhealthyEvent.class)
.subscribe(e -> log.error("All Redis nodes unhealthy after {} attempts: {}",
e.getFailedAttempts(), e.getUnhealthyDatabases()));
Health checks and failback
The examples above use the default health check strategy (PingStrategy), which
periodically sends a PING command to each
endpoint. Both clients also support a lag-aware strategy for Active-Active databases
and custom strategies, and both enable failback to the higher-weighted endpoint by
default. For the full set of failover, health check, and failback options, see the
client-specific documentation: