The premise

Lettuce is a Java driver package of Redis. When I first knew her, I encountered some problems when using RedisTemplate to Debug some underlying source code, and found that the driver package of spring-data-Redis was replaced with Lettuce after a certain version. Lettuce translated as Lettuce, yes, the kind of Lettuce you eat, so its Logo looks like this:

Since it can be recognized by Spring Ecology, Lettuce must have some advantages over others, so I spend time reading her official documents, arranging test examples, and writing this article. RELEASE, SpringBoot 2.1.8.RELEASE, JDK [8,11]. Extremely long warning: This article took off and on two weeks to complete and is over 40,000 words…..

Introduction of Lettuce

Lettuce is a high-performance Java based Redis driver framework, the bottom layer is integrated with Project Reactor to provide natural reactive programming, communication framework is integrated with Netty and uses non-blocking IO, 5.x version is integrated with the asynchronous programming features of JDK1.8, While ensuring high performance, it provides a rich and easy-to-use API. The new features of 5.1 are as follows:

  • supportRedisCommand added toZPOPMIN, ZPOPMAX, BZPOPMIN, BZPOPMAX.
  • Supported byBraveModule trackingRedisCommand execution.
  • supportRedis Streams.
  • Support asynchronous master/slave connections.
  • Support asynchronous connection pooling.
  • The new command can be executed at most once (automatic reconnection disabled).
  • Timeout Settings for global commands (also valid for asynchronous and reactive commands).
  • . , etc.

Note: Redis requires at least 2.6, but the higher the better. API compatibility is strong.

Just 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>
Copy the code
  • Gradle
Dependencies {compile 'IO. Lettuce :lettuce-core:5.1.8.RELEASE'}Copy the code

Connect the Redis

The connection to Redis in stand-alone, sentinel and cluster mode requires a unified standard to represent the connection details. In Lettuce, the unified standard is RedisURI. There are three ways to construct an instance of a RedisURI:

  • Custom stringsURIGrammar:
RedisURI uri = RedisURI.create("redis://localhost/");
Copy the code
  • Using the builder (RedisURI.Builder) :
RedisURI uri = RedisURI.builder().withHost("localhost").withPort(6379).build();
Copy the code
  • Instantiated directly through the constructor:
RedisURI uri = new RedisURI("localhost".6379.60, TimeUnit.SECONDS);
Copy the code

Custom connection URI syntax

  • Single-machine (the prefix isredis://)
Format: redis: / / / password @ host [: port] [[/ databaseNumber]? [timeout = timeout [d | h | m | s | | ms us | ns]] complete: Redis ://[email protected]:6379/0? Timeout =10sCopy the code
  • Single machine and in useSSL(prefix forrediss://<== == == == ==s
Format: redniss: / / / password @ host [: port] [[/ databaseNumber]? [timeout = timeout [d | h | m | s | | ms us | ns]] complete: Rediss ://[email protected]:6379/0? Timeout =10s Simple: rediss://localhostCopy the code
  • stand-aloneUnix Domain SocketsMode (the prefix isredis-socket://)
Format: redis - socket: / / path [? [timeout = timeout [d | h | m | s | | ms us | ns]] [& _database = database_]] : complete redis - socket: / / / TMP/redis? timeout=10s&_database=0Copy the code
  • Sentry (prefixed withredis-sentinel://)
Format: redis-sentinel://[password@]host[:port][,host2[:port2]][/databaseNumber][?[timeout=timeout[d|h|m|s|ms|us|ns]]#sentinelMa SterId complete: redis - sentinel: / / [email protected]:6379127.00 0.1:6380/0? # mymaster timeout = 10 sCopy the code

Timeout period Unit:

  • D day
  • H hours
  • M minutes
  • S second
  • Ms milliseconds
  • Us microseconds
  • Ns nanoseconds

I personally recommend using the builder provided by RedisURI, since custom URIs are simple but prone to human error. Since I have no usage scenarios for SSL and Unix Domain sockets, I will not list them below.

The basic use

Lettuce relies on four main components when used:

  • RedisURI: Connection information.
  • RedisClient:RedisThe client, specifically, cluster connection has a customRedisClusterClient.
  • Connection:RedisConnection, primarilyStatefulConnectionorStatefulRedisConnectionAs a subclass of, the type of connection is mainly determined by the specific mode of connection (stand-alone, sentinel, cluster, subscribe to publish, and so on) and is more important.
  • RedisCommands:RedisThe commandAPIInterface,It basically coversRedisAll commands for the distribution, which provides synchronization (sync), asynchronous (async), the equation (reative), for the user, will often followRedisCommandsSeries interfaces.

A basic use example is as follows:

@Test
public void testSetGet(a) throws Exception {
    RedisURI redisUri = RedisURI.builder()                    // <1> Create the connection information for the single-machine connection
            .withHost("localhost")
            .withPort(6379)
            .withTimeout(Duration.of(10, ChronoUnit.SECONDS))
            .build();
    RedisClient redisClient = RedisClient.create(redisUri);   // <2> Create the 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 operating
    connection.close();   // <5> Close the connection
    redisClient.shutdown();  // <6> Close the client
}
Copy the code

Note:

  • < 5 >: Close the connection before the application is stopped, one of an applicationRedisThe driver instance does not require many connections (usually only one connection instance is needed, if you have multiple connections you can consider using connection pooling, in factRedisCurrently, the module that handles the command is single threaded, and multithreaded calls with multiple connections on the client theoretically have no effect).
  • <6> : Close the client Normally before the application is stopped, if conditions permit, based on the first on first closed principle, the client should be closed after the connection is closed.

API

Lettuce provides three apis:

  • Synchronous (sync) :RedisCommands.
  • Asynchronous (async) :RedisAsyncCommands.
  • Equation (reactive) :RedisReactiveCommands.

First prepare a stand-alone Redis connection standby:

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

@BeforeClass
public static void beforeClass(a) {
    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(a) throws Exception {
    CONNECTION.close();
    CLIENT.shutdown();
}
Copy the code

An implementation of the Redis command API can be obtained directly from the StatefulRedisConnection instance. See the interface definition:

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

    boolean isMulti(a);

    RedisCommands<K, V> sync(a);

    RedisAsyncCommands<K, V> async(a);

    RedisReactiveCommands<K, V> reactive(a);
}    
Copy the code

Note that the StatefulRedisConnection instance created by RedisClient is generally the generic instance StatefulRedisConnection

without specifying the codec RedisCodec. That is, all command apis have String keys and values, which can be used in most scenarios. Of course, you can customize the codec RedisCodec

if necessary.
,v>
,string>

Synchronous API

First build the RedisCommands instance:

private static RedisCommands<String, String> COMMAND;

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

Basic use:

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


@Test
public void testSyncSetAndGet(a) 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
Copy the code

The synchronization API returns results immediately after all commands are called. If you are familiar with Jedis, the use of RedisCommands is not very different.

Asynchronous API

First build the RedisAsyncCommands instance:

private static RedisAsyncCommands<String, String> ASYNC_COMMAND;

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

Basic use:

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

All RedisAsyncCommands method execution returns a RedisFuture instance, and the RedisFuture interface is defined as follows:

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

    String getError(a);

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

That is, a RedisFuture can seamlessly use the methods provided by a Future or CompletableFuture introduced in JDK1.8. Here’s an example:

@Test
public void testAsyncSetAndGet1(a) 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 returns :{}", value));
    // Future#get()
    future.get();
}
// The Set command returns OK

@Test
public void testAsyncSetAndGet2(a) 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 returns :{}", s);
                                log.info("Get command returns :{}", g);
                            });
    result.get();
}
// The Set command returns OK
// The Get command returns throwable
Copy the code

If you can skillfully use CompletableFuture and functional programming skills, you can combine multiple RedisFutures to complete a series 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 by yourself.

Build the RedisReactiveCommands instance:

private static RedisReactiveCommands<String, String> REACTIVE_COMMAND;

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

According to Project Reactor, the method of RedisReactiveCommands returns Mono if the result contains only 0 or 1 elements, and Flux if the result contains 0 to N (N greater than 0) elements. Here’s an example:

@Test
public void testReactivePing(a) 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(a) 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 returns :{}", value));
    Thread.sleep(1000);
}
// The Get command returns throwable

@Test
public void testReactiveSet(a) 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
Copy the code

A more complex example includes transactions, function conversions, etc. :

@Test
public void testReactiveFunctional(a) 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
Copy the code

This method starts a transaction by setting counter to 1 and incrementing counter by 1.

Publish and subscribe

The cluster mode of publish-subscribe depends on the custom connection StatefulRedisPubSubConnection, cluster mode of publish-subscribe StatefulRedisClusterPubSubConnection depends on the custom connections, RedisClient#connectPubSub() and RedisClusterClient#connectPubSub(), respectively:

  • Non-cluster mode:
// It may be a single machine, common master/slave, sentinel, and other non-clustered 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()Copy the code
  • Cluster mode:
// The usage is basically the same as the non-clustered mode
RedisClusterClient clusterClient = ...
StatefulRedisClusterPubSubConnection<String, String> connection = clusterClient.connectPubSub();
connection.addListener(new RedisPubSubListener<String, String>() { ... });
RedisPubSubCommands<String, String> sync = connection.sync();
sync.subscribe("channel");
// ...
Copy the code

An example of Redis Keyspace Notifications can be found in the format of a stand-alone sync command:

@Test
public void testSyncKeyspaceNotification(a) 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 events when the key expires
    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
Copy the code

In fact, when implementing RedisPubSubListener, you can separate it out and try not to design it as an anonymous inner class.

Transaction and batch command execution

The transaction-related commands are WATCH, UNWATCH, EXEC, MULTI, and DISCARD, and there are methods in the RedisCommands interface. Here’s an example:

// Synchronization mode
@Test
public void testSyncMulti(a) 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
Copy the code

Redis Pipeline is the Pipeline mechanism can be understood as multiple commands packaged in a request sent to the Redis server, and then Redis server all the response results packaged a one-time return, thus saving unnecessary network resources (the most important is to reduce the number of network requests). Redis does not specify how the Pipeline mechanism is implemented, nor does it provide special commands to support the Pipeline mechanism. The bottom layer of Jedis is BIO (blocking IO) communication, so the way it works is that the client cache the commands to be sent, and then it needs to trigger and synchronously send a huge command list packet, and then receive and parse a huge response list packet. Pipeline is transparent to users in Lettuce. As the underlying communication framework is Netty, optimization at the network communication level does not need much intervention. In other words, it can be understood as follows: Netty helps Lettuce realize the Redis Pipeline mechanism from the bottom. However, the asynchronous API of Lettuce also provides a manual Flush method:

@Test
public void testAsyncManualFlush(a) {
    // Cancel automatic 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
Copy the code

The above is just some theoretical terms seen from the document, but the reality is thin, compared with the method provided by Jedis Pipeline, found that the execution time of Jedis Pipeline is lower:

@Test
public void testJedisPipeline(a) 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
Copy the code

Personally, it is speculated that all commands may not be sent together at the bottom (or even sent in a single message), and the location may require packet capture. From this point of view, if there is a scenario where a large number of Redis commands are executed, it is possible to use the Jedis Pipeline.

Note: The above tests infer that the executePipelined() method of the RedisTemplate is a fake Pipeline execution method. Be aware of this when using the RedisTemplate.

Lua script execution

The synchronization interface for executing the Lua command of Redis in Lettuce 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(a);

    String scriptKill(a);

    String scriptLoad(V var1);

    String digest(V var1);
}
Copy the code

The asynchronous and reactive interface method definitions are similar, except for the return value type. The usual methods are eval(), evalsha(), and scriptLoad(). Here’s 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(a) 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
Copy the code

High availability and sharding

To ensure high availability of Redis, the common Master/Replica mode (referred to here by the author as the common Master/Replica mode), sentinel, and cluster are generally adopted. That is, only Master/Replica replication is performed and manual switchover is required for faults. The normal master-slave mode can run independently or in conjunction with sentinel, which provides automatic failover and master node promotion. The MasterSlave can be used by both the master and the sentry, and the Connection instance can be obtained with the input of a RedisClient, a codec, and one or more redisuris.

Note here that if the method provided in MasterSlave only requires passing in a RedisURI instance, then Lettuce will carry out topology discovery mechanism to automatically obtain the information of master and slave nodes of Redis. If a collection of Redisuris is required to be passed in, then all node information is static to the normal master-slave mode and is not discovered or updated.

Topology discovery rules are as follows:

  • For ordinary master/slave (Master/Replica) patterns, which do not require perceptionRedisURIIf it points to the slave or master node, a one-time topology lookup is performed for all node information. After that, the node information is stored in a static cache and will not be updated.
  • In sentinel mode, you subscribe to all Sentinel instances and listen for subscribe/publish messages to trigger a topology refresh mechanism that updates cached node information, meaning that sentinel naturally discovers node information dynamically and does not support static configuration.

The API for topology discovery is TopologyProvider. To understand the principle of the API, refer to the specific implementation.

For Cluster mode, Lettuce provides a separate API.

In addition, if the connection is directed at a non-single Redis node, the connection instance provides the data read node preference (ReadFrom) setting, the optional values are:

  • MASTER: the only fromMasterNode.
  • MASTER_PREFERREDFrom:MasterNode.
  • SLAVE_PREFERREDFrom:SlavorNode.
  • SLAVE: the only fromSlavorNode.
  • NEAREST: Used the last connectionRedisInstance read.

Normal master-slave mode

Suppose there are now three Redis services forming a tree of master-slave relationships as follows:

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

For the first dynamic node discovery of node information in master/slave mode, the following connection needs to be established:

@Test
public void testDynamicReplica(a) throws Exception {
    // You only need to configure the connection information for one node, not necessarily for the master node, but for 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);
    // Only read data from the slave node
    connection.setReadFrom(ReadFrom.SLAVE);
    // Execute other Redis commands
    connection.close();
    redisClient.shutdown();
}
Copy the code

If you need to specify static Redis master-slave connection properties, you can build the connection like this:

@Test
public void testStaticReplica(a) 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);
    // Only read data from the primary node
    connection.setReadFrom(ReadFrom.MASTER);
    // Execute other Redis commands
    connection.close();
    redisClient.shutdown();
}
Copy the code

The guard mode

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

@Test
public void testDynamicSentinel(a) throws Exception {
    RedisURI redisUri = RedisURI.builder()
            .withPassword("Your password.")
            .withSentinel("localhost".26379)
            .withSentinelMasterId("ID of sentry Master")
            .build();
    RedisClient redisClient = RedisClient.create();
    StatefulRedisMasterSlaveConnection<String, String> connection = MasterSlave.connect(redisClient, new Utf8StringCodec(), redisUri);
    // Only data can be read from the slave 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
Copy the code

Cluster pattern

In view of the author is not familiar with the Redis Cluster mode, the USE of API in the Cluster mode itself has more restrictions, so here just briefly introduce how to use. A few features first:

The following apis provide cross-slot invocation functionality:

  • RedisAdvancedClusterCommands.
  • RedisAdvancedClusterAsyncCommands.
  • RedisAdvancedClusterReactiveCommands.

Static node selection:

  • masters: Select all the primary nodes to execute the command.
  • slaves: Select all slave nodes to execute the command, which is read-only mode.
  • all nodesThe: command can be executed on all nodes.

Dynamically update the cluster topology view:

  • Manual update, active invocationRedisClusterClient#reloadPartitions().
  • Background updates regularly.
  • Adaptive update, based on connection disconnection andMOVED/ASKCommand to redirect automatic updates.

Redis cluster setup detailed process can refer to the official document, assuming 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 => Active node in slot 0-5460.
  • 192.168.56.200:7002 => Active node in slot 5461-10922.
  • 192.168.56.200:7003 => Active node in slot 10923-16383.
  • 192.168.56.200:7004 => Secondary node of 7001.
  • 192.168.56.200:7005 => Secondary node 7002.
  • 192.168.56.200:7006 => Secondary node 7003.

Simple cluster connection and usage are as follows:

@Test
public void testSyncCluster(a){
    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
Copy the code

Node selection:

@Test
public void testSyncNodeSelection(a) {
    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(); / / the master node
    // The secondary node is read-only
    NodeSelection<String, String> replicas = commands.slaves();
    NodeSelectionCommands<String, String> nodeSelectionCommands = replicas.commands();
    // This is just a demonstration. You should disable the keys * command
    Executions<List<String>> keys = nodeSelectionCommands.keys("*");
    keys.forEach(key -> log.info("key: {}", key));
    connection.close();
    redisClusterClient.shutdown();
}
Copy the code

Update the cluster topology view periodically (every 10 minutes) :

@Test
public void testPeriodicClusterTopology(a) 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();
}
Copy the code

Update the cluster topology view adaptively:

@Test
public void testAdaptiveClusterTopology(a) 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();
}
Copy the code

Dynamic commands and custom commands

Custom commands are a finite set of Redis commands, but you can specify a more fine-grained KEY, ARGV, command type, codec, and return value type, depending on the dispatch() method:

// Customize the PING method
@Test
public void testCustomPing(a) 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

// Implement a custom Set method
@Test
public void testCustomSet(a) 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
Copy the code

Dynamic command is based on the Redis command finite set, and through annotations and dynamic proxy to complete some complex command combination implementation. Main annotations in IO. Lettuce. Core. Dynamic. The annotation packet path. Here’s a quick 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(a) 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]
Copy the code

Advanced features

There are many high-level use features of Lettuce, here are only two commonly used ones in my opinion:

  • Configure client resources.
  • Use connection pooling.

See the official documentation for more features.

Configure client resources

Client resource Settings are related to the performance, concurrency, and event handling of Lettuce. Thread pools or thread groups (EventLoopGroups and EventexecutorGroups) account for most of the client’s resource configuration, and 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 should be shut down when they are no longer used. In my opinion, client resources are Netty oriented. Note: Unless you are particularly familiar with, or have spent a lot of time testing and tweaking the parameters mentioned below, you may stumble if you change the default values intuitively without experience.

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

Build the DefaultClientResources instance:

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

/ / to builder
ClientResources resources = DefaultClientResources.builder()
                        .ioThreadPoolSize(4)
                        .computationThreadPoolSize(4)
                        .build()
Copy the code

Use:

ClientResources resources = DefaultClientResources.create();
/ / not cluster
RedisClient client = RedisClient.create(resources, uri);
/ / cluster
RedisClusterClient clusterClient = RedisClusterClient.create(resources, uris);
/ /...
client.shutdown();
clusterClient.shutdown();
// Close the resource
resources.shutdown();
Copy the code

Basic client resource configuration:

attribute describe The default value
ioThreadPoolSize I/OThe number of threads Runtime.getRuntime().availableProcessors()
computationThreadPoolSize Number of task threads Runtime.getRuntime().availableProcessors()

Advanced client resource configuration:

attribute describe The default value
eventLoopGroupProvider EventLoopGroupprovider
eventExecutorGroupProvider EventExecutorGroupprovider
eventBus Event bus DefaultEventBus
commandLatencyCollectorOptions Command delay collector configuration DefaultCommandLatencyCollectorOptions
commandLatencyCollector Command delay collector DefaultCommandLatencyCollector
commandLatencyPublisherOptions Command delay publisher configuration DefaultEventPublisherOptions
dnsResolver DNSThe processor The JDK orNettyprovide
reconnectDelay Reconnection delay configuration Delay.exponential()
nettyCustomizer NettyCustom configurator
tracing Track recorder

Non-clustered clientRedisClientProperties of:

The Redis non-cluster client RedisClient itself provides a method for configuring properties:

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

Configuration property list of non-clustered clients:

attribute describe The default value
pingBeforeActivateConnection Whether to execute before connection activationPINGThe command false
autoReconnect Whether to automatically reconnect true
cancelCommandsOnReconnectFailure Reconnection failure Whether to reject command execution false
suspendReconnectOnProtocolFailure The underlying protocol fails Whether to suspend the crane connection operation false
requestQueueSize Request queue size 2147483647(Integer#MAX_VALUE)
disconnectedBehavior Behavior when disconnected DEFAULT
sslOptions The SSL configuration
socketOptions Socketconfiguration 10 seconds Connection-Timeout, no keep-alive, no TCP noDelay
timeoutOptions The timeout configuration
publishOnScheduler A scheduler that publishes reactive signal data useI/Othread

Configure cluster client properties:

The Redis cluster client, RedisClusterClient, provides the following configuration methods:

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

client.setOptions(ClusterClientOptions.builder()
                       .topologyRefreshOptions(topologyRefreshOptions)
                       .build());
Copy the code

Cluster client configuration property list:

attribute describe The default value
enablePeriodicRefresh Whether to periodically update the cluster topology view false
refreshPeriod Update the cluster topology view periodically 60 seconds
enableAdaptiveRefreshTrigger Example Set the trigger for adaptive updating the cluster topology viewRefreshTrigger
adaptiveRefreshTriggersTimeout Automatically updates trigger timeout Settings in the cluster topology view 30 seconds
refreshTriggersReconnectAttempts Adaptive update the number of reconnections triggered by the cluster topology view 5
dynamicRefreshSources Whether to allow dynamic refreshing of topology resources true
closeStaleConnections Whether to allow stale connections to be closed true
maxRedirects Maximum number of cluster redirections 5
validateClusterNodeMembership Whether to verify the membership of cluster nodes true

Using connection pooling

To introduce a connection pool that relies on Commons-pool2:

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

The basic usage is as follows:

@Test
public void testUseConnectionPool(a) 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();
}
Copy the code

Among them, the synchronous connection pooling support need to use ConnectionPoolSupport, asynchronous connection pooling support need to use AsyncConnectionPoolSupport (Lettuce5.1 before support).

A few common examples of progressive deletion

Progressively remove domain-properties from the Hash:

@Test
public void testDelBigHashKey(a) 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 progressively removing elements of the Hash...");
    int counter = 0;
    do {
        MapScanCursor<String, String> result = COMMAND.hscan(key, cursor, scanArgs);
        // Reset the 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("After progressively removing the Hash element, the 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");
}
Copy the code

Progressively remove elements from a collection:

@Test
public void testDelBigSetKey(a) 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 progressively removing elements of Set...");
    int counter = 0;
    do {
        ValueScanCursor<String> result = COMMAND.sscan(key, cursor, scanArgs);
        // Reset the 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("The number of iterations :{}...", counter);
}

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

Progressively remove elements from an ordered collection:

@Test
public void testDelBigZSetKey(a) 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 progressively removing ZSet elements...");
    int counter = 0;
    do {
        ScoredValueScanCursor<String> result = COMMAND.zscan(key, cursor, scanArgs);
        // Reset the 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("Increments of ZSet elements are completed, 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");
}
Copy the code

Use Lettuce in SpringBoot

In my opinion, the API encapsulation in Spring-Data-Redis is not very good, it is heavy to use and not flexible enough. Here, combined with the previous example and code, we configure and integrate Lettuce in the SpringBoot scaffolding project. First introduce dependencies:

<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>        
Copy the code

In general, each application should use a single Redis client instance and a single connection instance. Here, a scaffold is designed to accommodate four scenarios: single-node, normal master-slave, sentinel, and cluster. For client resources, use the default implementation. For the connection attributes of Redis, the main attributes are Host, Port, and Password. The other attributes can be ignored temporarily. Based on the principle of convention over configuration, customize a series of property configuration classes (some configurations can be completely shared, but in order to clearly describe the relationship between classes, we split multiple configuration property classes and multiple configuration methods) :

@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 {}Copy the code

The configuration classes are as follows, using @conditionalonProperty for isolation. In general, very 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(a) {
        return DefaultClientResources.create();
    }

    @Bean
    @ConditionalOnProperty(name = "lettuce.single.host")
    public RedisURI singleRedisUri(a) {
        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(a) {
        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(a) {
        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(a) {
        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) {
        returnclusterClient.connect(); }}Copy the code

Finally, to enable IDE to recognize our configuration, we can add IDE affinity by adding a new file spring-configuration-metadata.json in/meta-inf folder. The content is as follows:

{
  "properties": [{"name": "lettuce.single"."type": "club.throwable.spring.lettuce.LettuceSingleProperties"."description": "Single Machine 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": "Sentinel 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"}}]Copy the code

If you want to make IDE affinity even better, you can add/meta-INF/ready-to-spring-configuration-metadata. json for more details. 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
Copy the code

summary

Based on the official documents of Lettuce, this paper makes a comprehensive analysis of its use, including some examples of its main functions and configuration. Limited by space, some features and configuration details are not analyzed. Lettuce has been accepted by Spring-Data-Redis as the official Redis client driver, so it is reliable. Some API designs of Lettuce are reasonable, with high scalability and flexibility. Personally, I suggest that it will be convenient to add configuration to SpringBoot application based on the Lettuce package, after all, RedisTemplate is too cumbersome, and also shields some advanced features and flexible APIS of Lettuce.

References:

  • Lettuce Reference Guide

(C-14-D E-A-20190928)

The technical public account (Throwable Digest), which will push the author’s original technical articles from time to time (never plagiarize or reprint) :