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:
- support
Redis
Command added toZPOPMIN, ZPOPMAX, BZPOPMIN, BZPOPMAX
. - Supported by
Brave
Module trackingRedis
Command execution. - support
Redis 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 strings
URI
Grammar:
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 is
redis://
)
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 use
SSL
(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-alone
Unix Domain Sockets
Mode (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 with
redis-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
:Redis
The client, specifically, cluster connection has a customRedisClusterClient
.Connection
:Redis
Connection, primarilyStatefulConnection
orStatefulRedisConnection
As 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
:Redis
The commandAPI
Interface,It basically coversRedis
All commands for the distribution, which provides synchronization (sync
), asynchronous (async
), the equation (reative
), for the user, will often followRedisCommands
Series 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 application
Redis
The 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 factRedis
Currently, 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.
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 perceptionRedisURI
If 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 fromMaster
Node.MASTER_PREFERRED
From:Master
Node.SLAVE_PREFERRED
From:Slavor
Node.SLAVE
: the only fromSlavor
Node.NEAREST
: Used the last connectionRedis
Instance 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 nodes
The: command can be executed on all nodes.
Dynamically update the cluster topology view:
- Manual update, active invocation
RedisClusterClient#reloadPartitions()
. - Background updates regularly.
- Adaptive update, based on connection disconnection and
MOVED/ASK
Command 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/O The number of threads |
Runtime.getRuntime().availableProcessors() |
computationThreadPoolSize |
Number of task threads | Runtime.getRuntime().availableProcessors() |
Advanced client resource configuration:
attribute | describe | The 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 The processor |
The JDK orNetty provide |
reconnectDelay |
Reconnection delay configuration | Delay.exponential() |
nettyCustomizer |
Netty Custom configurator |
– |
tracing |
Track recorder | – |
Non-clustered clientRedisClient
Properties 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 activationPING The 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 |
Socket configuration |
10 seconds Connection-Timeout, no keep-alive, no TCP noDelay |
timeoutOptions |
The timeout configuration | – |
publishOnScheduler |
A scheduler that publishes reactive signal data | useI/O thread |
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) :