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 the resilience4j dependencies. 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:

RATE THIS PAGE
Back to top ↑