Detailed explanation of Redis advanced client lettue

premise

Lattice is a Redis Java driver package. When I first met her, I encountered some problems when using RedisTemplate. Debug some source codes at the bottom. I found that the spring data Redis driver package was replaced with lattice after a certain version. Lettuce translates into lettuce. Yes, it's the kind of lettuce you eat, so its Logo looks like this:

Since lettue can be recognized by Spring ecology, it must be outstanding, so the author spent time reading her official documents, sorting out test examples and writing this article. The version used in writing this article is Lettuce 5.1.8 RELEASE,SpringBoot 2.1.8.RELEASE,JDK [8,11]. < Font color = Red > super long warning < / font >: This article took two weeks to complete intermittently, with more than 40000 words

Introduction to Lettuce

Lettuce is a high-performance Redis driven framework based on Java. The bottom layer integrates Project Reactor to provide natural reactive programming. The communication framework integrates Netty and uses non blocking IO. 5 Jdk1.0 is integrated after the X version The asynchronous programming feature of 8 provides a very rich and easy-to-use API while ensuring high performance. The new features of version 5.1 are as follows:

  • Supports the newly added commands of Redis: ZPOPMIN, ZPOPMAX, BZPOPMIN, BZPOPMAX.
  • It supports tracking Redis command execution through the brake module.
  • Redis Streams is supported.
  • Support asynchronous master-slave connection.
  • Support asynchronous connection pool.
  • The newly added command can execute the mode at most once (automatic reconnection is prohibited).
  • Global command timeout setting (also valid for asynchronous and reactive commands).
  • ...... wait

Note: the version of Redis needs at least 2.6. Of course, the higher the better. The API compatibility is relatively strong.

You just need to introduce a single dependency to start using Lettuce happily:

  • Maven
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>5.1.8.RELEASE</version>
</dependency>
  • Gradle
dependencies {
  compile 'io.lettuce:lettuce-core:5.1.8.RELEASE'
}

Connect to Redis

To connect Redis in stand-alone, sentinel and cluster modes, a unified standard is required to represent the connection details. In lettue, this unified standard is RedisURI. There are three ways to construct a RedisURI instance:

  • Custom string URI syntax:
RedisURI uri = RedisURI.create("redis://localhost/");
  • Use Builder (RedisURI.Builder):
RedisURI uri = RedisURI.builder().withHost("localhost").withPort(6379).build();
  • Instantiate directly through constructor:
RedisURI uri = new RedisURI("localhost", 6379, 60, TimeUnit.SECONDS);

Custom connection URI syntax

  • Stand alone (prefix redis: / /)
Format: redis://[password@]host[:port][/databaseNumber][?[timeout=timeout[d|h|m|s|ms|us|ns]]
Complete: redis://mypassword@127.0.0.1:6379/0?timeout=10s
 Simple: redis://localhost
  • Stand alone and use SSL (prefix rediss: / /) < = = note that there is more s after it
Format: rediss://[password@]host[:port][/databaseNumber][?[timeout=timeout[d|h|m|s|ms|us|ns]]
Complete: rediss://mypassword@127.0.0.1:6379/0?timeout=10s
 Simple: rediss://localhost
  • Single machine Unix Domain Sockets mode (prefix redis socket: / /)
Format: redis-socket://path[?[timeout=timeout[d|h|m|s|ms|us|ns]][&_database=database_]]
Complete: redis-socket:///tmp/redis?timeout=10s&_database=0
  • Sentinel (prefix redis sentinel: / /)
Format: redis-sentinel://[password@]host[:port][,host2[:port2]][/databaseNumber][?[timeout=timeout[d|h|m|s|ms|us|ns]]#sentinelMasterId
 Complete: redis-sentinel://mypassword@127.0.0.1:6379,127.0.0.1:6380/0?timeout=10s#mymaster

Timeout unit:

  • d days
  • h hours
  • Minutes m
  • s seconds
  • ms MS
  • us microseconds
  • ns nanosecond

Personally, I suggest using the builder provided by RedisURI. After all, although the customized URI is concise, it is prone to human errors. Since the author does not have the use scenario of SSL and Unix Domain Socket, these two connection methods are not listed below.

Basic use

Lettue relies on four main components when used:

  • RedisURI: connection information.
  • RedisClient: Redis client. In particular, the cluster connection has a customized RedisClusterClient.
  • Connection: Redis connection is mainly a subclass of StatefulConnection or StatefulRedisConnection. The connection type is mainly selected by the specific connection method (stand-alone, sentinel, cluster, subscription and publication, etc.), which is more important.
  • RedisCommands: RedisCommands API interface, which basically covers all commands of Redis distribution version, provides synchronous, async hronous and reactive call methods. For users, they will often deal with RedisCommands series interfaces.

A basic use example is as follows:

@Test
public void testSetGet() throws Exception {
    RedisURI redisUri = RedisURI.builder()                    // <1> Create connection information for stand-alone connections
            .withHost("localhost")
            .withPort(6379)
            .withTimeout(Duration.of(10, ChronoUnit.SECONDS))
            .build();
    RedisClient redisClient = RedisClient.create(redisUri);   // <2> Create client
    StatefulRedisConnection<String, String> connection = redisClient.connect();     // <3> Create a thread safe connection
    RedisCommands<String, String> redisCommands = connection.sync();                // <4> Create synchronization command
    SetArgs setArgs = SetArgs.Builder.nx().ex(5);
    String result = redisCommands.set("name", "throwable", setArgs);
    Assertions.assertThat(result).isEqualToIgnoringCase("OK");
    result = redisCommands.get("name");
    Assertions.assertThat(result).isEqualTo("throwable");
    // ...  Other operations
    connection.close();   // <5> Close connection
    redisClient.shutdown();  // <6> Close client
}

be careful:

  • <5> : closing a connection is usually performed before the application stops. A Redis driver instance in an application does not need too many connections (generally, only one connection instance is required. If there are multiple connections, you can consider using the connection pool. In fact, Redis currently processes commands in a single thread, which has no effect on multiple connections and multithreaded calls at the client in theory).
  • <6> : closing the client usually operates before the application stops. If conditions permit, based on the principle of opening later and closing first, the client should be closed after the connection is closed.

API

Lettue mainly provides three API s:

  • sync: RedisCommands.
  • async: RedisAsyncCommands.
  • reactive: redisreactive commands.

Prepare a stand-alone Redis connection for standby:

private static StatefulRedisConnection<String, String> CONNECTION;
private static RedisClient CLIENT;

@BeforeClass
public static void beforeClass() {
    RedisURI redisUri = RedisURI.builder()
            .withHost("localhost")
            .withPort(6379)
            .withTimeout(Duration.of(10, ChronoUnit.SECONDS))
            .build();
    CLIENT = RedisClient.create(redisUri);
    CONNECTION = CLIENT.connect();
}

@AfterClass
public static void afterClass() throws Exception {
    CONNECTION.close();
    CLIENT.shutdown();
}

The specific implementation of Redis command API can be obtained directly from StatefulRedisConnection instance. See its interface definition:

public interface StatefulRedisConnection<K, V> extends StatefulConnection<K, V> {

    boolean isMulti();

    RedisCommands<K, V> sync();

    RedisAsyncCommands<K, V> async();

    RedisReactiveCommands<K, V> reactive();
}    

It is worth noting that, without specifying the codec RedisCodec, the StatefulRedisConnection instance created by RedisClient is generally a generic instance StatefulRedisConnection < String, String >, that is, the KEY and VALUE of all command API s are of String type, which can meet most usage scenarios. Of course, you can customize the codec RedisCodec < K, V > when necessary.

Synchronization API

Build RedisCommands instance first:

private static RedisCommands<String, String> COMMAND;

@BeforeClass
public static void beforeClass() {
    COMMAND = CONNECTION.sync();
}

Basic usage:

@Test
public void testSyncPing() throws Exception {
   String pong = COMMAND.ping();
   Assertions.assertThat(pong).isEqualToIgnoringCase("PONG");
}


@Test
public void testSyncSetAndGet() throws Exception {
    SetArgs setArgs = SetArgs.Builder.nx().ex(5);
    COMMAND.set("name", "throwable", setArgs);
    String value = COMMAND.get("name");
    log.info("Get value: {}", value);
}

// Get value: throwable

The synchronization API returns results immediately after all command calls. If you are familiar with Jedis, the usage of RedisCommands is not much different from it.

Asynchronous API

First build the RedisAsyncCommands instance:

private static RedisAsyncCommands<String, String> ASYNC_COMMAND;

@BeforeClass
public static void beforeClass() {
    ASYNC_COMMAND = CONNECTION.async();
}

Basic usage:

@Test
public void testAsyncPing() throws Exception {
    RedisFuture<String> redisFuture = ASYNC_COMMAND.ping();
    log.info("Ping result:{}", redisFuture.get());
}
// Ping result:PONG

The returned results of all RedisAsyncCommands methods are RedisFuture instances, and the definition of RedisFuture interface is as follows:

public interface RedisFuture<V> extends CompletionStage<V>, Future<V> {

    String getError();

    boolean await(long timeout, TimeUnit unit) throws InterruptedException;
}    

That is, RedisFuture can seamlessly use Future or jdk1 The method provided by completable Future introduced in 8. for instance:

@Test
public void testAsyncSetAndGet1() throws Exception {
    SetArgs setArgs = SetArgs.Builder.nx().ex(5);
    RedisFuture<String> future = ASYNC_COMMAND.set("name", "throwable", setArgs);
    // CompletableFuture#thenAccept()
    future.thenAccept(value -> log.info("Set Command return:{}", value));
    // Future#get()
    future.get();
}
// Set command returns: OK

@Test
public void testAsyncSetAndGet2() throws Exception {
    SetArgs setArgs = SetArgs.Builder.nx().ex(5);
    CompletableFuture<Void> result =
            (CompletableFuture<Void>) ASYNC_COMMAND.set("name", "throwable", setArgs)
                    .thenAcceptBoth(ASYNC_COMMAND.get("name"),
                            (s, g) -> {
                                log.info("Set Command return:{}", s);
                                log.info("Get Command return:{}", g);
                            });
    result.get();
}
// Set command returns: OK
// Get command returns: throwable

If you can skillfully use completable future and functional programming skills, you can combine multiple RedisFuture to complete some columns of complex operations.

Reactive API

The reactive programming framework introduced by Lettuce is Project Reactor , if you have no experience in reactive programming, you can learn about Project Reactor first.

Build RedisReactiveCommands instance:

private static RedisReactiveCommands<String, String> REACTIVE_COMMAND;

@BeforeClass
public static void beforeClass() {
    REACTIVE_COMMAND = CONNECTION.reactive();
}

According to the method of Project Reactor and RedisReactiveCommands, if the returned result contains only 0 or 1 elements, the return value type is Mono. If the returned result contains 0 to N (N is greater than 0), the return value is Flux. for instance:

@Test
public void testReactivePing() throws Exception {
    Mono<String> ping = REACTIVE_COMMAND.ping();
    ping.subscribe(v -> log.info("Ping result:{}", v));
    Thread.sleep(1000);
}
// Ping result:PONG

@Test
public void testReactiveSetAndGet() throws Exception {
    SetArgs setArgs = SetArgs.Builder.nx().ex(5);
    REACTIVE_COMMAND.set("name", "throwable", setArgs).block();
    REACTIVE_COMMAND.get("name").subscribe(value -> log.info("Get Command return:{}", value));
    Thread.sleep(1000);
}
// Get command returns: throwable

@Test
public void testReactiveSet() throws Exception {
    REACTIVE_COMMAND.sadd("food", "bread", "meat", "fish").block();
    Flux<String> flux = REACTIVE_COMMAND.smembers("food");
    flux.subscribe(log::info);
    REACTIVE_COMMAND.srem("food", "bread", "meat", "fish").block();
    Thread.sleep(1000);
}
// meat
// bread
// fish

Take a more complex example, including transactions, function conversion, etc

@Test
public void testReactiveFunctional() throws Exception {
    REACTIVE_COMMAND.multi().doOnSuccess(r -> {
        REACTIVE_COMMAND.set("counter", "1").doOnNext(log::info).subscribe();
        REACTIVE_COMMAND.incr("counter").doOnNext(c -> log.info(String.valueOf(c))).subscribe();
    }).flatMap(s -> REACTIVE_COMMAND.exec())
            .doOnNext(transactionResult -> log.info("Discarded:{}", transactionResult.wasDiscarded()))
            .subscribe();
    Thread.sleep(1000);
}
// OK
// 2
// Discarded:false

This method starts a transaction. First set the counter to 1, and then increase the counter by 1.

Publish And Subscribe

Publishing and subscribing in non cluster mode depends on the customized connection StatefulRedisPubSubConnection. Publishing and subscribing in cluster mode depends on the customized connection StatefulRedisClusterPubSubConnection. They come from RedisClient#connectPubSub() series methods and RedisClusterClient#connectPubSub():

  • Non cluster mode:
// It may be a single machine, ordinary master-slave, sentinel and other non cluster clients
RedisClient client = ...
StatefulRedisPubSubConnection<String, String> connection = client.connectPubSub();
connection.addListener(new RedisPubSubListener<String, String>() { ... });

// Synchronization command
RedisPubSubCommands<String, String> sync = connection.sync();
sync.subscribe("channel");

// Asynchronous command
RedisPubSubAsyncCommands<String, String> async = connection.async();
RedisFuture<Void> future = async.subscribe("channel");

// Reactive command
RedisPubSubReactiveCommands<String, String> reactive = connection.reactive();
reactive.subscribe("channel").subscribe();

reactive.observeChannels().doOnNext(patternMessage -> {...}).subscribe()
  • Cluster mode:
// The usage mode is basically the same as that of non cluster mode
RedisClusterClient clusterClient = ...
StatefulRedisClusterPubSubConnection<String, String> connection = clusterClient.connectPubSub();
connection.addListener(new RedisPubSubListener<String, String>() { ... });
RedisPubSubCommands<String, String> sync = connection.sync();
sync.subscribe("channel");
// ...

Here, a Redis key space notification is given in the mode of single machine synchronization command( Redis Keyspace Notifications )Examples:

@Test
public void testSyncKeyspaceNotification() throws Exception {
    RedisURI redisUri = RedisURI.builder()
            .withHost("localhost")
            .withPort(6379)
            // Note that this can only be library 0
            .withDatabase(0)
            .withTimeout(Duration.of(10, ChronoUnit.SECONDS))
            .build();
    RedisClient redisClient = RedisClient.create(redisUri);
    StatefulRedisConnection<String, String> redisConnection = redisClient.connect();
    RedisCommands<String, String> redisCommands = redisConnection.sync();
    // Receive only key expiration events
    redisCommands.configSet("notify-keyspace-events", "Ex");
    StatefulRedisPubSubConnection<String, String> connection = redisClient.connectPubSub();
    connection.addListener(new RedisPubSubAdapter<>() {

        @Override
        public void psubscribed(String pattern, long count) {
            log.info("pattern:{},count:{}", pattern, count);
        }

        @Override
        public void message(String pattern, String channel, String message) {
            log.info("pattern:{},channel:{},message:{}", pattern, channel, message);
        }
    });
    RedisPubSubCommands<String, String> commands = connection.sync();
    commands.psubscribe("__keyevent@0__:expired");
    redisCommands.setex("name", 2, "throwable");
    Thread.sleep(10000);
    redisConnection.close();
    connection.close();
    redisClient.shutdown();
}
// pattern:__keyevent@0__:expired,count:1
// pattern:__keyevent@0__:expired,channel:__keyevent@0__:expired,message:name

In fact, when implementing RedisPubSubListener, it can be separated separately. Try not to design it in the form of anonymous internal classes.

Transaction and batch command execution

The transaction related commands are WATCH, UNWATCH, EXEC, MULTI and DISCARD. There are corresponding methods in the RedisCommands series interface. for instance:

// Synchronous mode
@Test
public void testSyncMulti() throws Exception {
    COMMAND.multi();
    COMMAND.setex("name-1", 2, "throwable");
    COMMAND.setex("name-2", 2, "doge");
    TransactionResult result = COMMAND.exec();
    int index = 0;
    for (Object r : result) {
        log.info("Result-{}:{}", index, r);
        index++;
    }
}
// Result-0:OK
// Result-1:OK

Redis's pipeline mechanism can be understood as that multiple commands are packaged in one request and sent to the redis server, and then the redis server packages all the response results and returns them at one time, so as to save unnecessary network resources (mainly reducing the number of network requests). Redis has no clear regulations on how to implement the pipeline mechanism, nor does it provide special commands to support the pipeline mechanism. The bottom layer of Jedis adopts BIO (blocking IO) communication, so its approach is that the client caches the commands to be sent, and finally needs to trigger, then synchronously send a huge command list package, and then receive and parse a huge response list package. Pipeline is transparent to users in lattice. Since the underlying communication framework is Netty, the optimization of network communication does not need too much intervention. In other words, Netty helps lattice implement redis's pipeline mechanism from the bottom. However, lettue's asynchronous API also provides a manual Flush method:

@Test
public void testAsyncManualFlush() {
    // Cancel auto flush
    ASYNC_COMMAND.setAutoFlushCommands(false);
    List<RedisFuture<?>> redisFutures = Lists.newArrayList();
    int count = 5000;
    for (int i = 0; i < count; i++) {
        String key = "key-" + (i + 1);
        String value = "value-" + (i + 1);
        redisFutures.add(ASYNC_COMMAND.set(key, value));
        redisFutures.add(ASYNC_COMMAND.expire(key, 2));
    }
    long start = System.currentTimeMillis();
    ASYNC_COMMAND.flushCommands();
    boolean result = LettuceFutures.awaitAll(10, TimeUnit.SECONDS, redisFutures.toArray(new RedisFuture[0]));
    Assertions.assertThat(result).isTrue();
    log.info("Lettuce cost:{} ms", System.currentTimeMillis() - start);
}
// Lettuce cost:1302 ms

The above is only some theoretical terms seen from the documents, but the reality is skinny. By comparing the methods provided by Jedis Pipeline, it is found that the execution time of Jedis Pipeline is relatively low:

@Test
public void testJedisPipeline() throws Exception {
    Jedis jedis = new Jedis();
    Pipeline pipeline = jedis.pipelined();
    int count = 5000;
    for (int i = 0; i < count; i++) {
        String key = "key-" + (i + 1);
        String value = "value-" + (i + 1);
        pipeline.set(key, value);
        pipeline.expire(key, 2);
    }
    long start = System.currentTimeMillis();
    pipeline.syncAndReturnAll();
    log.info("Jedis cost:{} ms", System.currentTimeMillis()  - start);
}
// Jedis cost:9 ms

I guess that lettue may not combine all commands at the bottom and send them at one time (or even send a single command). Specifically, it may need to capture packets to locate. From this point of view, if there are a large number of scenarios in which Redis commands are executed, you might as well use Jedis Pipeline.

Note: it is inferred from the above test that the executePipelined() method of RedisTemplate is a < strong > < font color = Red > fake < / font > < / strong > pipeline execution method. Please pay attention to this when using RedisTemplate.

Lua script execution

The synchronization interface for executing the Lua command of Redis in lattice is as follows:

public interface RedisScriptingCommands<K, V> {

    <T> T eval(String var1, ScriptOutputType var2, K... var3);

    <T> T eval(String var1, ScriptOutputType var2, K[] var3, V... var4);

    <T> T evalsha(String var1, ScriptOutputType var2, K... var3);

    <T> T evalsha(String var1, ScriptOutputType var2, K[] var3, V... var4);

    List<Boolean> scriptExists(String... var1);

    String scriptFlush();

    String scriptKill();

    String scriptLoad(V var1);

    String digest(V var1);
}

The definitions of asynchronous and reactive interface methods are similar. The difference is the return value type. Generally, we often use eval(), evalsha() and scriptLoad() methods. Take a simple example:

private static RedisCommands<String, String> COMMANDS;
private static String RAW_LUA = "local key = KEYS[1]\n" +
        "local value = ARGV[1]\n" +
        "local timeout = ARGV[2]\n" +
        "redis.call('SETEX', key, tonumber(timeout), value)\n" +
        "local result = redis.call('GET', key)\n" +
        "return result;";
private static AtomicReference<String> LUA_SHA = new AtomicReference<>();

@Test
public void testLua() throws Exception {
    LUA_SHA.compareAndSet(null, COMMANDS.scriptLoad(RAW_LUA));
    String[] keys = new String[]{"name"};
    String[] args = new String[]{"throwable", "5000"};
    String result = COMMANDS.evalsha(LUA_SHA.get(), ScriptOutputType.VALUE, keys, args);
    log.info("Get value:{}", result);
}
// Get value:throwable

High availability and fragmentation

For the high availability of Redis, the common master-slave mode (Master/Replica, which I call the common master-slave mode here, that is, only the master-slave replication is done, and manual switching is required for failure), sentinel and cluster are generally adopted. The normal master-slave mode can operate independently or cooperate with the sentinel, but the sentinel provides automatic failover and master node promotion functions. Ordinary master-slave and sentinel can use MasterSlave to obtain the corresponding Connection instance through the input parameters including RedisClient, codec and one or more redisuris.

Note that if the method provided in MasterSlave requires only one RedisURI instance to be passed in, then lattice will use the topology discovery mechanism to automatically obtain the Redis master-slave node information; If it is required to pass in a RedisURI collection, all node information is static for the normal master-slave mode and will not be discovered and updated.

The rules of topology discovery are as follows:

  • For the normal master-slave (Master/Replica) mode, there is no need to sense whether the RedisURI points to the slave node or the master node. Only one-time topology search will be conducted to find all node information. After that, the node information will be saved in the static cache and will not be updated.
  • For sentinel mode, it will subscribe to all sentinel instances and listen to subscription / publish messages to trigger topology refresh mechanism and update cached node information, that is, sentinel naturally finds node information dynamically and does not support static configuration.

The API of topology discovery mechanism is TopologyProvider. If you need to understand its principle, you can refer to the specific implementation.

For the Cluster mode, Lettuce provides a set of independent API s.

In addition, if the lattice connection is for a non single Redis node, the connection instance provides the data reading node preference (ReadFrom) setting. The optional values are:

  • Master: read only from the master node.
  • Master_ Err: read from Master priority node.
  • SLAVE_PREFERRED: read from slave node preferentially.
  • SLAVE: only read from SLAVE node.
  • NEAREST: read using the Redis instance connected last time.

General master-slave mode

Suppose that three Redis services form a tree like master-slave relationship as follows:

  • Node 1: localhost:6379, and the role is Master.
  • Node 2: localhost:6380, the role is slave, and the slave node of node 1.
  • Node 3: localhost:6381, the role is slave, and the slave node of node 2.

For the first dynamic node discovery, the node information of master-slave mode needs to be connected as follows:

@Test
public void testDynamicReplica() throws Exception {
    // Here, you only need to configure the connection information of one node, not necessarily the information of the master node, but also the slave node
    RedisURI uri = RedisURI.builder().withHost("localhost").withPort(6379).build();
    RedisClient redisClient = RedisClient.create(uri);
    StatefulRedisMasterSlaveConnection<String, String> connection = MasterSlave.connect(redisClient, new Utf8StringCodec(), uri);
    // Read data from nodes only
    connection.setReadFrom(ReadFrom.SLAVE);
    // Execute other Redis commands
    connection.close();
    redisClient.shutdown();
}

If you need to specify the static Redis master-slave node connection attribute, you can build the connection as follows:

@Test
public void testStaticReplica() throws Exception {
    List<RedisURI> uris = new ArrayList<>();
    RedisURI uri1 = RedisURI.builder().withHost("localhost").withPort(6379).build();
    RedisURI uri2 = RedisURI.builder().withHost("localhost").withPort(6380).build();
    RedisURI uri3 = RedisURI.builder().withHost("localhost").withPort(6381).build();
    uris.add(uri1);
    uris.add(uri2);
    uris.add(uri3);
    RedisClient redisClient = RedisClient.create();
    StatefulRedisMasterSlaveConnection<String, String> connection = MasterSlave.connect(redisClient,
            new Utf8StringCodec(), uris);
    // Read data from master node only
    connection.setReadFrom(ReadFrom.MASTER);
    // Execute other Redis commands
    connection.close();
    redisClient.shutdown();
}

Sentinel mode

Since lattice itself provides a sentinel topology discovery mechanism, you only need to configure a RedisURI instance of the sentinel node:

@Test
public void testDynamicSentinel() throws Exception {
    RedisURI redisUri = RedisURI.builder()
            .withPassword("Your password")
            .withSentinel("localhost", 26379)
            .withSentinelMasterId("sentry Master of ID")
            .build();
    RedisClient redisClient = RedisClient.create();
    StatefulRedisMasterSlaveConnection<String, String> connection = MasterSlave.connect(redisClient, new Utf8StringCodec(), redisUri);
    // Only data is allowed to be read from the node
    connection.setReadFrom(ReadFrom.SLAVE);
    RedisCommands<String, String> command = connection.sync();
    SetArgs setArgs = SetArgs.Builder.nx().ex(5);
    command.set("name", "throwable", setArgs);
    String value = command.get("name");
    log.info("Get value:{}", value);
}
// Get value:throwable

Cluster mode

Since the author is not familiar with Redis Cluster mode, there are many restrictions on the use of API in Cluster mode, so here is only a brief introduction to how to use it. Let's start with a few features:

The following API provides the function of calling across slots:

  • RedisAdvancedClusterCommands.
  • RedisAdvancedClusterAsyncCommands.
  • RedisAdvancedClusterReactiveCommands.

Static node selection function:

  • masters: select all master nodes to execute commands.
  • Slave: select all slave nodes to execute commands, which is actually read-only mode.
  • All nodes: the command can be executed on all nodes.

Cluster topology view dynamic update function:

  • Manually update and actively call RedisClusterClient#reloadPartitions().
  • The background is updated regularly.
  • Adaptive update, automatic update based on disconnection and redirection of MOVED/ASK command.

Refer to the official documents for the detailed process of setting up Redis cluster. It is assumed that the cluster has been set up as follows (192.168.56.200 is the author's virtual machine Host):

  • 192.168.56.200:7001 = > master node, slot 0-5460.
  • 192.168.56.200:7002 = > master node, slot 5461-10922.
  • 192.168.56.200:7003 = > master node, slot 10923-16383.
  • 192.168.56.200:7004 = > 7001 slave node.
  • 192.168.56.200:7005 = > 7002 slave node.
  • 192.168.56.200:7006 = > 7003 slave node.

Simple cluster connection and usage are as follows:

@Test
public void testSyncCluster(){
    RedisURI uri = RedisURI.builder().withHost("192.168.56.200").build();
    RedisClusterClient redisClusterClient = RedisClusterClient.create(uri);
    StatefulRedisClusterConnection<String, String> connection = redisClusterClient.connect();
    RedisAdvancedClusterCommands<String, String> commands = connection.sync();
    commands.setex("name",10, "throwable");
    String value = commands.get("name");
    log.info("Get value:{}", value);
}
// Get value:throwable

Node selection:

@Test
public void testSyncNodeSelection() {
    RedisURI uri = RedisURI.builder().withHost("192.168.56.200").withPort(7001).build();
    RedisClusterClient redisClusterClient = RedisClusterClient.create(uri);
    StatefulRedisClusterConnection<String, String> connection = redisClusterClient.connect();
    RedisAdvancedClusterCommands<String, String> commands = connection.sync();
//  commands.all();  //  All nodes
//  commands.masters();  //  Master node
    // Read only from node
    NodeSelection<String, String> replicas = commands.slaves();
    NodeSelectionCommands<String, String> nodeSelectionCommands = replicas.commands();
    // This is just a demonstration. Generally, the keys * command should be disabled
    Executions<List<String>> keys = nodeSelectionCommands.keys("*");
    keys.forEach(key -> log.info("key: {}", key));
    connection.close();
    redisClusterClient.shutdown();
}

Regularly update the cluster topology view (every ten minutes, which should be considered by ourselves and not too frequently):

@Test
public void testPeriodicClusterTopology() throws Exception {
    RedisURI uri = RedisURI.builder().withHost("192.168.56.200").withPort(7001).build();
    RedisClusterClient redisClusterClient = RedisClusterClient.create(uri);
    ClusterTopologyRefreshOptions options = ClusterTopologyRefreshOptions
            .builder()
            .enablePeriodicRefresh(Duration.of(10, ChronoUnit.MINUTES))
            .build();
    redisClusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(options).build());
    StatefulRedisClusterConnection<String, String> connection = redisClusterClient.connect();
    RedisAdvancedClusterCommands<String, String> commands = connection.sync();
    commands.setex("name", 10, "throwable");
    String value = commands.get("name");
    log.info("Get value:{}", value);
    Thread.sleep(Integer.MAX_VALUE);
    connection.close();
    redisClusterClient.shutdown();
}

Adaptively update the cluster topology view:

@Test
public void testAdaptiveClusterTopology() throws Exception {
    RedisURI uri = RedisURI.builder().withHost("192.168.56.200").withPort(7001).build();
    RedisClusterClient redisClusterClient = RedisClusterClient.create(uri);
    ClusterTopologyRefreshOptions options = ClusterTopologyRefreshOptions.builder()
            .enableAdaptiveRefreshTrigger(
                    ClusterTopologyRefreshOptions.RefreshTrigger.MOVED_REDIRECT,
                    ClusterTopologyRefreshOptions.RefreshTrigger.PERSISTENT_RECONNECTS
            )
            .adaptiveRefreshTriggersTimeout(Duration.of(30, ChronoUnit.SECONDS))
            .build();
    redisClusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(options).build());
    StatefulRedisClusterConnection<String, String> connection = redisClusterClient.connect();
    RedisAdvancedClusterCommands<String, String> commands = connection.sync();
    commands.setex("name", 10, "throwable");
    String value = commands.get("name");
    log.info("Get value:{}", value);
    Thread.sleep(Integer.MAX_VALUE);
    connection.close();
    redisClusterClient.shutdown();
}

Dynamic and custom commands

Custom commands are a limited set of Redis commands, but you can specify KEY, ARGV, command type, codec and return value type in a finer granularity, depending on the dispatch() method:

// Custom implementation PING method
@Test
public void testCustomPing() throws Exception {
    RedisURI redisUri = RedisURI.builder()
            .withHost("localhost")
            .withPort(6379)
            .withTimeout(Duration.of(10, ChronoUnit.SECONDS))
            .build();
    RedisClient redisClient = RedisClient.create(redisUri);
    StatefulRedisConnection<String, String> connect = redisClient.connect();
    RedisCommands<String, String> sync = connect.sync();
    RedisCodec<String, String> codec = StringCodec.UTF8;
    String result = sync.dispatch(CommandType.PING, new StatusOutput<>(codec));
    log.info("PING:{}", result);
    connect.close();
    redisClient.shutdown();
}
// PING:PONG

// Custom implementation Set method
@Test
public void testCustomSet() throws Exception {
    RedisURI redisUri = RedisURI.builder()
            .withHost("localhost")
            .withPort(6379)
            .withTimeout(Duration.of(10, ChronoUnit.SECONDS))
            .build();
    RedisClient redisClient = RedisClient.create(redisUri);
    StatefulRedisConnection<String, String> connect = redisClient.connect();
    RedisCommands<String, String> sync = connect.sync();
    RedisCodec<String, String> codec = StringCodec.UTF8;
    sync.dispatch(CommandType.SETEX, new StatusOutput<>(codec),
            new CommandArgs<>(codec).addKey("name").add(5).addValue("throwable"));
    String result = sync.get("name");
    log.info("Get value:{}", result);
    connect.close();
    redisClient.shutdown();
}
// Get value:throwable

Dynamic commands are based on the limited set of Redis commands, and some complex command combinations are realized through annotations and dynamic agents. The main comments are in io lettuce. core. dynamic. Annotation package path. For example:

public interface CustomCommand extends Commands {

    // SET [key] [value]
    @Command("SET ?0 ?1")
    String setKey(String key, String value);

    // SET [key] [value]
    @Command("SET :key :value")
    String setKeyNamed(@Param("key") String key, @Param("value") String value);

    // MGET [key1] [key2]
    @Command("MGET ?0 ?1")
    List<String> mGet(String key1, String key2);
    /**
     * Method name as command
     */
    @CommandNaming(strategy = CommandNaming.Strategy.METHOD_NAME)
    String mSet(String key1, String value1, String key2, String value2);
}


@Test
public void testCustomDynamicSet() throws Exception {
    RedisURI redisUri = RedisURI.builder()
            .withHost("localhost")
            .withPort(6379)
            .withTimeout(Duration.of(10, ChronoUnit.SECONDS))
            .build();
    RedisClient redisClient = RedisClient.create(redisUri);
    StatefulRedisConnection<String, String> connect = redisClient.connect();
    RedisCommandFactory commandFactory = new RedisCommandFactory(connect);
    CustomCommand commands = commandFactory.getCommands(CustomCommand.class);
    commands.setKey("name", "throwable");
    commands.setKeyNamed("throwable", "doge");
    log.info("MGET ===> " + commands.mGet("name", "throwable"));
    commands.mSet("key1", "value1","key2", "value2");
    log.info("MGET ===> " + commands.mGet("key1", "key2"));
    connect.close();
    redisClient.shutdown();
}
// MGET ===> [throwable, doge]
// MGET ===> [value1, value2]

Higher order characteristics

Lettue has many high-level features. Here are only two points I think are commonly used:

  • Configure client resources.
  • Use connection pool.

More other features can be found in the official documentation.

Configure client resources

The setting of client resources is related to the performance, concurrency and event processing of lettue. Thread pool or thread group related configurations occupy most of the client resource configuration (EventLoopGroups and eventexecutorgroups). These thread pools or thread groups are the basic components of the connector. In general, client resources should be shared among multiple Redis clients, and they need to be shut down when they are no longer in use. The author believes that client resources are Netty oriented. Note: < font color = Red > unless you are particularly familiar with or spend a long time testing and adjusting the parameters mentioned below, you may step on the pit if you change the default value intuitively without experience.

The client resource interface is ClientResources, and the implementation class is DefaultClientResources.

Build DefaultClientResources instance:

// default
ClientResources resources = DefaultClientResources.create();

// Builder
ClientResources resources = DefaultClientResources.builder()
                        .ioThreadPoolSize(4)
                        .computationThreadPoolSize(4)
                        .build()

use:

ClientResources resources = DefaultClientResources.create();
// Non cluster
RedisClient client = RedisClient.create(resources, uri);
// colony
RedisClusterClient clusterClient = RedisClusterClient.create(resources, uris);
// ......
client.shutdown();
clusterClient.shutdown();
// close resource
resources.shutdown();

Basic configuration of client resources:

attribute describe Default value
ioThreadPoolSize I/O threads Runtime.getRuntime().availableProcessors()
computationThreadPoolSize Number of task threads Runtime.getRuntime().availableProcessors()

Advanced configuration of client resources:

attribute describe Default value
eventLoopGroupProvider EventLoopGroup provider -
eventExecutorGroupProvider EventExecutorGroup provider -
eventBus Event bus DefaultEventBus
commandLatencyCollectorOptions Command delay collector configuration DefaultCommandLatencyCollectorOptions
commandLatencyCollector Command delay collector DefaultCommandLatencyCollector
commandLatencyPublisherOptions Command delay publisher configuration DefaultEventPublisherOptions
dnsResolver DNS processor Provided by JDK or Netty
reconnectDelay Reconnection delay configuration Delay.exponential()
nettyCustomizer Netty custom Configurator -
tracing Track recorder -

Attribute configuration of non cluster client RedisClient:

Redis non cluster client RedisClient itself provides the method of configuring attributes:

RedisClient client = RedisClient.create(uri);
client.setOptions(ClientOptions.builder()
                       .autoReconnect(false)
                       .pingBeforeActivateConnection(true)
                       .build());

List of configuration properties of non cluster clients:

attribute describe Default value
pingBeforeActivateConnection Whether to execute PING command before connection activation false
autoReconnect Automatic reconnection true
cancelCommandsOnReconnectFailure Reconnection failed. Reject command execution false
suspendReconnectOnProtocolFailure Underlying protocol failed. Suspend reconnection false
requestQueueSize Request queue capacity 2147483647(Integer#MAX_VALUE)
disconnectedBehavior Behavior when losing connection DEFAULT
sslOptions SSL configuration -
socketOptions Socket configuration 10 seconds Connection-Timeout, no keep-alive, no TCP noDelay
timeoutOptions Timeout configuration -
publishOnScheduler Scheduler for publishing reactive signal data Using I/O threads

Cluster client attribute configuration:

Redis cluster client RedisClusterClient itself provides the method of configuring attributes:

RedisClusterClient client = RedisClusterClient.create(uri);
ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                .enablePeriodicRefresh(refreshPeriod(10, TimeUnit.MINUTES))
                .enableAllAdaptiveRefreshTriggers()
                .build();

client.setOptions(ClusterClientOptions.builder()
                       .topologyRefreshOptions(topologyRefreshOptions)
                       .build());

List of configuration attributes of cluster client:

attribute describe Default value
enablePeriodicRefresh Whether to allow periodic updating of cluster topology view false
refreshPeriod Update cluster topology view cycle 60 seconds
enableAdaptiveRefreshTrigger Set the adaptive update cluster topology view trigger RefreshTrigger -
adaptiveRefreshTriggersTimeout Adaptive update cluster topology view trigger timeout setting 30 seconds
refreshTriggersReconnectAttempts Number of reconnections triggered by adaptively updating the cluster topology view 5
dynamicRefreshSources Allow dynamic refresh of topology resources true
closeStaleConnections Allow stale connections to be closed true
maxRedirects Maximum number of cluster redirects 5
validateClusterNodeMembership Verify the membership of cluster nodes true

Use connection pool

Introducing connection pool dependency commons-pool2:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.7.0</version>
</dependency

The basic usage is as follows:

@Test
public void testUseConnectionPool() throws Exception {
    RedisURI redisUri = RedisURI.builder()
            .withHost("localhost")
            .withPort(6379)
            .withTimeout(Duration.of(10, ChronoUnit.SECONDS))
            .build();
    RedisClient redisClient = RedisClient.create(redisUri);
    GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
    GenericObjectPool<StatefulRedisConnection<String, String>> pool
            = ConnectionPoolSupport.createGenericObjectPool(redisClient::connect, poolConfig);
    try (StatefulRedisConnection<String, String> connection = pool.borrowObject()) {
        RedisCommands<String, String> command = connection.sync();
        SetArgs setArgs = SetArgs.Builder.nx().ex(5);
        command.set("name", "throwable", setArgs);
        String n = command.get("name");
        log.info("Get value:{}", n);
    }
    pool.close();
    redisClient.shutdown();
}

Among them, ConnectionPoolSupport is required for the pooling support of synchronous connections, and AsyncConnectionPoolSupport is required for the pooling support of asynchronous connections (only supported after lettue5.1).

Several common examples of progressive deletion

Gradually delete domain attributes in Hash:

@Test
public void testDelBigHashKey() throws Exception {
    // SCAN parameters
    ScanArgs scanArgs = ScanArgs.Builder.limit(2);
    // TEMP cursor
    ScanCursor cursor = ScanCursor.INITIAL;
    // Target KEY
    String key = "BIG_HASH_KEY";
    prepareHashTestData(key);
    log.info("Start progressive deletion Hash Element of...");
    int counter = 0;
    do {
        MapScanCursor<String, String> result = COMMAND.hscan(key, cursor, scanArgs);
        // Reset TEMP cursor
        cursor = ScanCursor.of(result.getCursor());
        cursor.setFinished(result.isFinished());
        Collection<String> fields = result.getMap().values();
        if (!fields.isEmpty()) {
            COMMAND.hdel(key, fields.toArray(new String[0]));
        }
        counter++;
    } while (!(ScanCursor.FINISHED.getCursor().equals(cursor.getCursor()) && ScanCursor.FINISHED.isFinished() == cursor.isFinished()));
    log.info("Progressive deletion Hash Complete element,Number of iterations:{} ...", counter);
}

private void prepareHashTestData(String key) throws Exception {
    COMMAND.hset(key, "1", "1");
    COMMAND.hset(key, "2", "2");
    COMMAND.hset(key, "3", "3");
    COMMAND.hset(key, "4", "4");
    COMMAND.hset(key, "5", "5");
}

Progressive deletion of elements in the collection:

@Test
public void testDelBigSetKey() throws Exception {
    String key = "BIG_SET_KEY";
    prepareSetTestData(key);
    // SCAN parameters
    ScanArgs scanArgs = ScanArgs.Builder.limit(2);
    // TEMP cursor
    ScanCursor cursor = ScanCursor.INITIAL;
    log.info("Start progressive deletion Set Element of...");
    int counter = 0;
    do {
        ValueScanCursor<String> result = COMMAND.sscan(key, cursor, scanArgs);
        // Reset TEMP cursor
        cursor = ScanCursor.of(result.getCursor());
        cursor.setFinished(result.isFinished());
        List<String> values = result.getValues();
        if (!values.isEmpty()) {
            COMMAND.srem(key, values.toArray(new String[0]));
        }
        counter++;
    } while (!(ScanCursor.FINISHED.getCursor().equals(cursor.getCursor()) && ScanCursor.FINISHED.isFinished() == cursor.isFinished()));
    log.info("Progressive deletion Set Complete element,Number of iterations:{} ...", counter);
}

private void prepareSetTestData(String key) throws Exception {
    COMMAND.sadd(key, "1", "2", "3", "4", "5");
}

Progressive deletion of elements in an ordered collection:

@Test
public void testDelBigZSetKey() throws Exception {
    // SCAN parameters
    ScanArgs scanArgs = ScanArgs.Builder.limit(2);
    // TEMP cursor
    ScanCursor cursor = ScanCursor.INITIAL;
    // Target KEY
    String key = "BIG_ZSET_KEY";
    prepareZSetTestData(key);
    log.info("Start progressive deletion ZSet Element of...");
    int counter = 0;
    do {
        ScoredValueScanCursor<String> result = COMMAND.zscan(key, cursor, scanArgs);
        // Reset TEMP cursor
        cursor = ScanCursor.of(result.getCursor());
        cursor.setFinished(result.isFinished());
        List<ScoredValue<String>> scoredValues = result.getValues();
        if (!scoredValues.isEmpty()) {
            COMMAND.zrem(key, scoredValues.stream().map(ScoredValue<String>::getValue).toArray(String[]::new));
        }
        counter++;
    } while (!(ScanCursor.FINISHED.getCursor().equals(cursor.getCursor()) && ScanCursor.FINISHED.isFinished() == cursor.isFinished()));
    log.info("Progressive deletion ZSet Complete element,Number of iterations:{} ...", counter);
}

private void prepareZSetTestData(String key) throws Exception {
    COMMAND.zadd(key, 0, "1");
    COMMAND.zadd(key, 0, "2");
    COMMAND.zadd(key, 0, "3");
    COMMAND.zadd(key, 0, "4");
    COMMAND.zadd(key, 0, "5");
}

Using Lettuce in SpringBoot

Personally, I don't think the API encapsulation in spring data redis is very good. It's heavy to use and not flexible enough. Here, combined with the previous examples and codes, configure and integrate Lettuce in the SpringBoot scaffold project. Import dependency first:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.1.8.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
            <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
        <version>5.1.8.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.10</version>
        <scope>provided</scope>
    </dependency>
</dependencies>        

Generally, each application should use a single Redis client instance and a single connection instance. Here, a scaffold is designed to adapt to four use scenarios: single machine, ordinary master-slave, sentry and cluster. For client resources, the default implementation can be adopted. For Redis connection properties, the main ones are Host, Port and Password. Others can be ignored temporarily. Based on the principle that convention is greater than configuration, customize a series of attribute configuration classes first (in fact, some configurations can be completely shared, but considering the relationship between classes to be clearly described, multiple configuration attribute classes and multiple configuration methods are split here):

@Data
@ConfigurationProperties(prefix = "lettuce")
public class LettuceProperties {

    private LettuceSingleProperties single;
    private LettuceReplicaProperties replica;
    private LettuceSentinelProperties sentinel;
    private LettuceClusterProperties cluster;

}

@Data
public class LettuceSingleProperties {

    private String host;
    private Integer port;
    private String password;
}

@EqualsAndHashCode(callSuper = true)
@Data
public class LettuceReplicaProperties extends LettuceSingleProperties {

}

@EqualsAndHashCode(callSuper = true)
@Data
public class LettuceSentinelProperties extends LettuceSingleProperties {

    private String masterId;
}

@EqualsAndHashCode(callSuper = true)
@Data
public class LettuceClusterProperties extends LettuceSingleProperties {

}

The configuration classes are as follows, mainly using @ ConditionalOnProperty for isolation. Generally, few people will use more than one Redis connection scenario in an application:

@RequiredArgsConstructor
@Configuration
@ConditionalOnClass(name = "io.lettuce.core.RedisURI")
@EnableConfigurationProperties(value = LettuceProperties.class)
public class LettuceAutoConfiguration {

    private final LettuceProperties lettuceProperties;

    @Bean(destroyMethod = "shutdown")
    public ClientResources clientResources() {
        return DefaultClientResources.create();
    }

    @Bean
    @ConditionalOnProperty(name = "lettuce.single.host")
    public RedisURI singleRedisUri() {
        LettuceSingleProperties singleProperties = lettuceProperties.getSingle();
        return RedisURI.builder()
                .withHost(singleProperties.getHost())
                .withPort(singleProperties.getPort())
                .withPassword(singleProperties.getPassword())
                .build();
    }

    @Bean(destroyMethod = "shutdown")
    @ConditionalOnProperty(name = "lettuce.single.host")
    public RedisClient singleRedisClient(ClientResources clientResources, @Qualifier("singleRedisUri") RedisURI redisUri) {
        return RedisClient.create(clientResources, redisUri);
    }

    @Bean(destroyMethod = "close")
    @ConditionalOnProperty(name = "lettuce.single.host")
    public StatefulRedisConnection<String, String> singleRedisConnection(@Qualifier("singleRedisClient") RedisClient singleRedisClient) {
        return singleRedisClient.connect();
    }

    @Bean
    @ConditionalOnProperty(name = "lettuce.replica.host")
    public RedisURI replicaRedisUri() {
        LettuceReplicaProperties replicaProperties = lettuceProperties.getReplica();
        return RedisURI.builder()
                .withHost(replicaProperties.getHost())
                .withPort(replicaProperties.getPort())
                .withPassword(replicaProperties.getPassword())
                .build();
    }

    @Bean(destroyMethod = "shutdown")
    @ConditionalOnProperty(name = "lettuce.replica.host")
    public RedisClient replicaRedisClient(ClientResources clientResources, @Qualifier("replicaRedisUri") RedisURI redisUri) {
        return RedisClient.create(clientResources, redisUri);
    }

    @Bean(destroyMethod = "close")
    @ConditionalOnProperty(name = "lettuce.replica.host")
    public StatefulRedisMasterSlaveConnection<String, String> replicaRedisConnection(@Qualifier("replicaRedisClient") RedisClient replicaRedisClient,
                                                                                     @Qualifier("replicaRedisUri") RedisURI redisUri) {
        return MasterSlave.connect(replicaRedisClient, new Utf8StringCodec(), redisUri);
    }

    @Bean
    @ConditionalOnProperty(name = "lettuce.sentinel.host")
    public RedisURI sentinelRedisUri() {
        LettuceSentinelProperties sentinelProperties = lettuceProperties.getSentinel();
        return RedisURI.builder()
                .withPassword(sentinelProperties.getPassword())
                .withSentinel(sentinelProperties.getHost(), sentinelProperties.getPort())
                .withSentinelMasterId(sentinelProperties.getMasterId())
                .build();
    }

    @Bean(destroyMethod = "shutdown")
    @ConditionalOnProperty(name = "lettuce.sentinel.host")
    public RedisClient sentinelRedisClient(ClientResources clientResources, @Qualifier("sentinelRedisUri") RedisURI redisUri) {
        return RedisClient.create(clientResources, redisUri);
    }

    @Bean(destroyMethod = "close")
    @ConditionalOnProperty(name = "lettuce.sentinel.host")
    public StatefulRedisMasterSlaveConnection<String, String> sentinelRedisConnection(@Qualifier("sentinelRedisClient") RedisClient sentinelRedisClient,
                                                                                      @Qualifier("sentinelRedisUri") RedisURI redisUri) {
        return MasterSlave.connect(sentinelRedisClient, new Utf8StringCodec(), redisUri);
    }

    @Bean
    @ConditionalOnProperty(name = "lettuce.cluster.host")
    public RedisURI clusterRedisUri() {
        LettuceClusterProperties clusterProperties = lettuceProperties.getCluster();
        return RedisURI.builder()
                .withHost(clusterProperties.getHost())
                .withPort(clusterProperties.getPort())
                .withPassword(clusterProperties.getPassword())
                .build();
    }

    @Bean(destroyMethod = "shutdown")
    @ConditionalOnProperty(name = "lettuce.cluster.host")
    public RedisClusterClient redisClusterClient(ClientResources clientResources, @Qualifier("clusterRedisUri") RedisURI redisUri) {
        return RedisClusterClient.create(clientResources, redisUri);
    }

    @Bean(destroyMethod = "close")
    @ConditionalOnProperty(name = "lettuce.cluster")
    public StatefulRedisClusterConnection<String, String> clusterConnection(RedisClusterClient clusterClient) {
        return clusterClient.connect();
    }
}

Finally, in order for the IDE to recognize our configuration, you can add ide affinity, and add a file spring configuration metadata. In the / META-INF folder JSON, as follows:

{
  "properties": [
    {
      "name": "lettuce.single",
      "type": "club.throwable.spring.lettuce.LettuceSingleProperties",
      "description": "Stand alone configuration",
      "sourceType": "club.throwable.spring.lettuce.LettuceProperties"
    },
    {
      "name": "lettuce.replica",
      "type": "club.throwable.spring.lettuce.LettuceReplicaProperties",
      "description": "Master slave configuration",
      "sourceType": "club.throwable.spring.lettuce.LettuceProperties"
    },
    {
      "name": "lettuce.sentinel",
      "type": "club.throwable.spring.lettuce.LettuceSentinelProperties",
      "description": "Sentry configuration",
      "sourceType": "club.throwable.spring.lettuce.LettuceProperties"
    },
    {
      "name": "lettuce.single",
      "type": "club.throwable.spring.lettuce.LettuceClusterProperties",
      "description": "Cluster configuration",
      "sourceType": "club.throwable.spring.lettuce.LettuceProperties"
    }
  ]
}

If you want to make IDE affinity better, you can add / meta-inf / additional spring configuration metadata JSON for more detailed definition. Simple use is as follows:

@Slf4j
@Component
public class RedisCommandLineRunner implements CommandLineRunner {

    @Autowired
    @Qualifier("singleRedisConnection")
    private StatefulRedisConnection<String, String> connection;

    @Override
    public void run(String... args) throws Exception {
        RedisCommands<String, String> redisCommands = connection.sync();
        redisCommands.setex("name", 5, "throwable");
        log.info("Get value:{}", redisCommands.get("name"));
    }
}
// Get value:throwable

Summary

This paper is based on the official document of Lettuce, and makes a comprehensive analysis of its use, including some examples of main functions and configuration. Due to space limitation, some features and configuration details are not analyzed. Lattice has been accepted by spring data Redis as the official Redis client driver, so it is trustworthy. Some of its API designs are indeed reasonable, with high scalability and flexibility. Personally, it's easy to add configuration to SpringBoot application based on lattice package. After all, RedisTemplate is too cumbersome, and it also shields some advanced features and flexible APIs of lattice.

reference material:

link

(end of this article c-14-d e-a-20190928 too many things have happened recently...)

Tags: Java reactive

Posted by aeboi80 on Sat, 16 Apr 2022 19:21:35 +0930