premise
Lattice is a Redis Java driver package. When I first met her, I encountered some problems when using RedisTemplate. Debug some source codes at the bottom. I found that the spring data Redis driver package was replaced with lattice after a certain version. Lettuce translates into lettuce. Yes, it's the kind of lettuce you eat, so its Logo looks like this:
Since lettue can be recognized by Spring ecology, it must be outstanding, so the author spent time reading her official documents, sorting out test examples and writing this article. The version used in writing this article is Lettuce 5.1.8 RELEASE,SpringBoot 2.1.8.RELEASE,JDK [8,11]. < Font color = Red > super long warning < / font >: This article took two weeks to complete intermittently, with more than 40000 words
Introduction to Lettuce
Lettuce is a high-performance Redis driven framework based on Java. The bottom layer integrates Project Reactor to provide natural reactive programming. The communication framework integrates Netty and uses non blocking IO. 5 Jdk1.0 is integrated after the X version The asynchronous programming feature of 8 provides a very rich and easy-to-use API while ensuring high performance. The new features of version 5.1 are as follows:
- Supports the newly added commands of Redis: ZPOPMIN, ZPOPMAX, BZPOPMIN, BZPOPMAX.
- It supports tracking Redis command execution through the brake module.
- Redis Streams is supported.
- Support asynchronous master-slave connection.
- Support asynchronous connection pool.
- The newly added command can execute the mode at most once (automatic reconnection is prohibited).
- Global command timeout setting (also valid for asynchronous and reactive commands).
- ...... wait
Note: the version of Redis needs at least 2.6. Of course, the higher the better. The API compatibility is relatively strong.
You just need to introduce a single dependency to start using Lettuce happily:
- Maven
<dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>5.1.8.RELEASE</version> </dependency>
- Gradle
dependencies { compile 'io.lettuce:lettuce-core:5.1.8.RELEASE' }
Connect to Redis
To connect Redis in stand-alone, sentinel and cluster modes, a unified standard is required to represent the connection details. In lettue, this unified standard is RedisURI. There are three ways to construct a RedisURI instance:
- Custom string URI syntax:
RedisURI uri = RedisURI.create("redis://localhost/");
- Use Builder (RedisURI.Builder):
RedisURI uri = RedisURI.builder().withHost("localhost").withPort(6379).build();
- Instantiate directly through constructor:
RedisURI uri = new RedisURI("localhost", 6379, 60, TimeUnit.SECONDS);
Custom connection URI syntax
- Stand alone (prefix redis: / /)
Format: redis://[password@]host[:port][/databaseNumber][?[timeout=timeout[d|h|m|s|ms|us|ns]] Complete: redis://mypassword@127.0.0.1:6379/0?timeout=10s Simple: redis://localhost
- Stand alone and use SSL (prefix rediss: / /) < = = note that there is more s after it
Format: rediss://[password@]host[:port][/databaseNumber][?[timeout=timeout[d|h|m|s|ms|us|ns]] Complete: rediss://mypassword@127.0.0.1:6379/0?timeout=10s Simple: rediss://localhost
- Single machine Unix Domain Sockets mode (prefix redis socket: / /)
Format: redis-socket://path[?[timeout=timeout[d|h|m|s|ms|us|ns]][&_database=database_]] Complete: redis-socket:///tmp/redis?timeout=10s&_database=0
- Sentinel (prefix redis sentinel: / /)
Format: redis-sentinel://[password@]host[:port][,host2[:port2]][/databaseNumber][?[timeout=timeout[d|h|m|s|ms|us|ns]]#sentinelMasterId Complete: redis-sentinel://mypassword@127.0.0.1:6379,127.0.0.1:6380/0?timeout=10s#mymaster
Timeout unit:
- d days
- h hours
- Minutes m
- s seconds
- ms MS
- us microseconds
- ns nanosecond
Personally, I suggest using the builder provided by RedisURI. After all, although the customized URI is concise, it is prone to human errors. Since the author does not have the use scenario of SSL and Unix Domain Socket, these two connection methods are not listed below.
Basic use
Lettue relies on four main components when used:
- RedisURI: connection information.
- RedisClient: Redis client. In particular, the cluster connection has a customized RedisClusterClient.
- Connection: Redis connection is mainly a subclass of StatefulConnection or StatefulRedisConnection. The connection type is mainly selected by the specific connection method (stand-alone, sentinel, cluster, subscription and publication, etc.), which is more important.
- RedisCommands: RedisCommands API interface, which basically covers all commands of Redis distribution version, provides synchronous, async hronous and reactive call methods. For users, they will often deal with RedisCommands series interfaces.
A basic use example is as follows:
@Test public void testSetGet() throws Exception { RedisURI redisUri = RedisURI.builder() // <1> Create connection information for stand-alone connections .withHost("localhost") .withPort(6379) .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); RedisClient redisClient = RedisClient.create(redisUri); // <2> Create client StatefulRedisConnection<String, String> connection = redisClient.connect(); // <3> Create a thread safe connection RedisCommands<String, String> redisCommands = connection.sync(); // <4> Create synchronization command SetArgs setArgs = SetArgs.Builder.nx().ex(5); String result = redisCommands.set("name", "throwable", setArgs); Assertions.assertThat(result).isEqualToIgnoringCase("OK"); result = redisCommands.get("name"); Assertions.assertThat(result).isEqualTo("throwable"); // ... Other operations connection.close(); // <5> Close connection redisClient.shutdown(); // <6> Close client }
be careful:
- <5> : closing a connection is usually performed before the application stops. A Redis driver instance in an application does not need too many connections (generally, only one connection instance is required. If there are multiple connections, you can consider using the connection pool. In fact, Redis currently processes commands in a single thread, which has no effect on multiple connections and multithreaded calls at the client in theory).
- <6> : closing the client usually operates before the application stops. If conditions permit, based on the principle of opening later and closing first, the client should be closed after the connection is closed.
API
Lettue mainly provides three API s:
- sync: RedisCommands.
- async: RedisAsyncCommands.
- reactive: redisreactive commands.
Prepare a stand-alone Redis connection for standby:
private static StatefulRedisConnection<String, String> CONNECTION; private static RedisClient CLIENT; @BeforeClass public static void beforeClass() { RedisURI redisUri = RedisURI.builder() .withHost("localhost") .withPort(6379) .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); CLIENT = RedisClient.create(redisUri); CONNECTION = CLIENT.connect(); } @AfterClass public static void afterClass() throws Exception { CONNECTION.close(); CLIENT.shutdown(); }
The specific implementation of Redis command API can be obtained directly from StatefulRedisConnection instance. See its interface definition:
public interface StatefulRedisConnection<K, V> extends StatefulConnection<K, V> { boolean isMulti(); RedisCommands<K, V> sync(); RedisAsyncCommands<K, V> async(); RedisReactiveCommands<K, V> reactive(); }
It is worth noting that, without specifying the codec RedisCodec, the StatefulRedisConnection instance created by RedisClient is generally a generic instance StatefulRedisConnection < String, String >, that is, the KEY and VALUE of all command API s are of String type, which can meet most usage scenarios. Of course, you can customize the codec RedisCodec < K, V > when necessary.
Synchronization API
Build RedisCommands instance first:
private static RedisCommands<String, String> COMMAND; @BeforeClass public static void beforeClass() { COMMAND = CONNECTION.sync(); }
Basic usage:
@Test public void testSyncPing() throws Exception { String pong = COMMAND.ping(); Assertions.assertThat(pong).isEqualToIgnoringCase("PONG"); } @Test public void testSyncSetAndGet() throws Exception { SetArgs setArgs = SetArgs.Builder.nx().ex(5); COMMAND.set("name", "throwable", setArgs); String value = COMMAND.get("name"); log.info("Get value: {}", value); } // Get value: throwable
The synchronization API returns results immediately after all command calls. If you are familiar with Jedis, the usage of RedisCommands is not much different from it.
Asynchronous API
First build the RedisAsyncCommands instance:
private static RedisAsyncCommands<String, String> ASYNC_COMMAND; @BeforeClass public static void beforeClass() { ASYNC_COMMAND = CONNECTION.async(); }
Basic usage:
@Test public void testAsyncPing() throws Exception { RedisFuture<String> redisFuture = ASYNC_COMMAND.ping(); log.info("Ping result:{}", redisFuture.get()); } // Ping result:PONG
The returned results of all RedisAsyncCommands methods are RedisFuture instances, and the definition of RedisFuture interface is as follows:
public interface RedisFuture<V> extends CompletionStage<V>, Future<V> { String getError(); boolean await(long timeout, TimeUnit unit) throws InterruptedException; }
That is, RedisFuture can seamlessly use Future or jdk1 The method provided by completable Future introduced in 8. for instance:
@Test public void testAsyncSetAndGet1() throws Exception { SetArgs setArgs = SetArgs.Builder.nx().ex(5); RedisFuture<String> future = ASYNC_COMMAND.set("name", "throwable", setArgs); // CompletableFuture#thenAccept() future.thenAccept(value -> log.info("Set Command return:{}", value)); // Future#get() future.get(); } // Set command returns: OK @Test public void testAsyncSetAndGet2() throws Exception { SetArgs setArgs = SetArgs.Builder.nx().ex(5); CompletableFuture<Void> result = (CompletableFuture<Void>) ASYNC_COMMAND.set("name", "throwable", setArgs) .thenAcceptBoth(ASYNC_COMMAND.get("name"), (s, g) -> { log.info("Set Command return:{}", s); log.info("Get Command return:{}", g); }); result.get(); } // Set command returns: OK // Get command returns: throwable
If you can skillfully use completable future and functional programming skills, you can combine multiple RedisFuture to complete some columns of complex operations.
Reactive API
The reactive programming framework introduced by Lettuce is Project Reactor , if you have no experience in reactive programming, you can learn about Project Reactor first.
Build RedisReactiveCommands instance:
private static RedisReactiveCommands<String, String> REACTIVE_COMMAND; @BeforeClass public static void beforeClass() { REACTIVE_COMMAND = CONNECTION.reactive(); }
According to the method of Project Reactor and RedisReactiveCommands, if the returned result contains only 0 or 1 elements, the return value type is Mono. If the returned result contains 0 to N (N is greater than 0), the return value is Flux. for instance:
@Test public void testReactivePing() throws Exception { Mono<String> ping = REACTIVE_COMMAND.ping(); ping.subscribe(v -> log.info("Ping result:{}", v)); Thread.sleep(1000); } // Ping result:PONG @Test public void testReactiveSetAndGet() throws Exception { SetArgs setArgs = SetArgs.Builder.nx().ex(5); REACTIVE_COMMAND.set("name", "throwable", setArgs).block(); REACTIVE_COMMAND.get("name").subscribe(value -> log.info("Get Command return:{}", value)); Thread.sleep(1000); } // Get command returns: throwable @Test public void testReactiveSet() throws Exception { REACTIVE_COMMAND.sadd("food", "bread", "meat", "fish").block(); Flux<String> flux = REACTIVE_COMMAND.smembers("food"); flux.subscribe(log::info); REACTIVE_COMMAND.srem("food", "bread", "meat", "fish").block(); Thread.sleep(1000); } // meat // bread // fish
Take a more complex example, including transactions, function conversion, etc
@Test public void testReactiveFunctional() throws Exception { REACTIVE_COMMAND.multi().doOnSuccess(r -> { REACTIVE_COMMAND.set("counter", "1").doOnNext(log::info).subscribe(); REACTIVE_COMMAND.incr("counter").doOnNext(c -> log.info(String.valueOf(c))).subscribe(); }).flatMap(s -> REACTIVE_COMMAND.exec()) .doOnNext(transactionResult -> log.info("Discarded:{}", transactionResult.wasDiscarded())) .subscribe(); Thread.sleep(1000); } // OK // 2 // Discarded:false
This method starts a transaction. First set the counter to 1, and then increase the counter by 1.
Publish And Subscribe
Publishing and subscribing in non cluster mode depends on the customized connection StatefulRedisPubSubConnection. Publishing and subscribing in cluster mode depends on the customized connection StatefulRedisClusterPubSubConnection. They come from RedisClient#connectPubSub() series methods and RedisClusterClient#connectPubSub():
- Non cluster mode:
// It may be a single machine, ordinary master-slave, sentinel and other non cluster clients RedisClient client = ... StatefulRedisPubSubConnection<String, String> connection = client.connectPubSub(); connection.addListener(new RedisPubSubListener<String, String>() { ... }); // Synchronization command RedisPubSubCommands<String, String> sync = connection.sync(); sync.subscribe("channel"); // Asynchronous command RedisPubSubAsyncCommands<String, String> async = connection.async(); RedisFuture<Void> future = async.subscribe("channel"); // Reactive command RedisPubSubReactiveCommands<String, String> reactive = connection.reactive(); reactive.subscribe("channel").subscribe(); reactive.observeChannels().doOnNext(patternMessage -> {...}).subscribe()
- Cluster mode:
// The usage mode is basically the same as that of non cluster mode RedisClusterClient clusterClient = ... StatefulRedisClusterPubSubConnection<String, String> connection = clusterClient.connectPubSub(); connection.addListener(new RedisPubSubListener<String, String>() { ... }); RedisPubSubCommands<String, String> sync = connection.sync(); sync.subscribe("channel"); // ...
Here, a Redis key space notification is given in the mode of single machine synchronization command( Redis Keyspace Notifications )Examples:
@Test public void testSyncKeyspaceNotification() throws Exception { RedisURI redisUri = RedisURI.builder() .withHost("localhost") .withPort(6379) // Note that this can only be library 0 .withDatabase(0) .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); RedisClient redisClient = RedisClient.create(redisUri); StatefulRedisConnection<String, String> redisConnection = redisClient.connect(); RedisCommands<String, String> redisCommands = redisConnection.sync(); // Receive only key expiration events redisCommands.configSet("notify-keyspace-events", "Ex"); StatefulRedisPubSubConnection<String, String> connection = redisClient.connectPubSub(); connection.addListener(new RedisPubSubAdapter<>() { @Override public void psubscribed(String pattern, long count) { log.info("pattern:{},count:{}", pattern, count); } @Override public void message(String pattern, String channel, String message) { log.info("pattern:{},channel:{},message:{}", pattern, channel, message); } }); RedisPubSubCommands<String, String> commands = connection.sync(); commands.psubscribe("__keyevent@0__:expired"); redisCommands.setex("name", 2, "throwable"); Thread.sleep(10000); redisConnection.close(); connection.close(); redisClient.shutdown(); } // pattern:__keyevent@0__:expired,count:1 // pattern:__keyevent@0__:expired,channel:__keyevent@0__:expired,message:name
In fact, when implementing RedisPubSubListener, it can be separated separately. Try not to design it in the form of anonymous internal classes.
Transaction and batch command execution
The transaction related commands are WATCH, UNWATCH, EXEC, MULTI and DISCARD. There are corresponding methods in the RedisCommands series interface. for instance:
// Synchronous mode @Test public void testSyncMulti() throws Exception { COMMAND.multi(); COMMAND.setex("name-1", 2, "throwable"); COMMAND.setex("name-2", 2, "doge"); TransactionResult result = COMMAND.exec(); int index = 0; for (Object r : result) { log.info("Result-{}:{}", index, r); index++; } } // Result-0:OK // Result-1:OK
Redis's pipeline mechanism can be understood as that multiple commands are packaged in one request and sent to the redis server, and then the redis server packages all the response results and returns them at one time, so as to save unnecessary network resources (mainly reducing the number of network requests). Redis has no clear regulations on how to implement the pipeline mechanism, nor does it provide special commands to support the pipeline mechanism. The bottom layer of Jedis adopts BIO (blocking IO) communication, so its approach is that the client caches the commands to be sent, and finally needs to trigger, then synchronously send a huge command list package, and then receive and parse a huge response list package. Pipeline is transparent to users in lattice. Since the underlying communication framework is Netty, the optimization of network communication does not need too much intervention. In other words, Netty helps lattice implement redis's pipeline mechanism from the bottom. However, lettue's asynchronous API also provides a manual Flush method:
@Test public void testAsyncManualFlush() { // Cancel auto flush ASYNC_COMMAND.setAutoFlushCommands(false); List<RedisFuture<?>> redisFutures = Lists.newArrayList(); int count = 5000; for (int i = 0; i < count; i++) { String key = "key-" + (i + 1); String value = "value-" + (i + 1); redisFutures.add(ASYNC_COMMAND.set(key, value)); redisFutures.add(ASYNC_COMMAND.expire(key, 2)); } long start = System.currentTimeMillis(); ASYNC_COMMAND.flushCommands(); boolean result = LettuceFutures.awaitAll(10, TimeUnit.SECONDS, redisFutures.toArray(new RedisFuture[0])); Assertions.assertThat(result).isTrue(); log.info("Lettuce cost:{} ms", System.currentTimeMillis() - start); } // Lettuce cost:1302 ms
The above is only some theoretical terms seen from the documents, but the reality is skinny. By comparing the methods provided by Jedis Pipeline, it is found that the execution time of Jedis Pipeline is relatively low:
@Test public void testJedisPipeline() throws Exception { Jedis jedis = new Jedis(); Pipeline pipeline = jedis.pipelined(); int count = 5000; for (int i = 0; i < count; i++) { String key = "key-" + (i + 1); String value = "value-" + (i + 1); pipeline.set(key, value); pipeline.expire(key, 2); } long start = System.currentTimeMillis(); pipeline.syncAndReturnAll(); log.info("Jedis cost:{} ms", System.currentTimeMillis() - start); } // Jedis cost:9 ms
I guess that lettue may not combine all commands at the bottom and send them at one time (or even send a single command). Specifically, it may need to capture packets to locate. From this point of view, if there are a large number of scenarios in which Redis commands are executed, you might as well use Jedis Pipeline.
Note: it is inferred from the above test that the executePipelined() method of RedisTemplate is a < strong > < font color = Red > fake < / font > < / strong > pipeline execution method. Please pay attention to this when using RedisTemplate.
Lua script execution
The synchronization interface for executing the Lua command of Redis in lattice is as follows:
public interface RedisScriptingCommands<K, V> { <T> T eval(String var1, ScriptOutputType var2, K... var3); <T> T eval(String var1, ScriptOutputType var2, K[] var3, V... var4); <T> T evalsha(String var1, ScriptOutputType var2, K... var3); <T> T evalsha(String var1, ScriptOutputType var2, K[] var3, V... var4); List<Boolean> scriptExists(String... var1); String scriptFlush(); String scriptKill(); String scriptLoad(V var1); String digest(V var1); }
The definitions of asynchronous and reactive interface methods are similar. The difference is the return value type. Generally, we often use eval(), evalsha() and scriptLoad() methods. Take a simple example:
private static RedisCommands<String, String> COMMANDS; private static String RAW_LUA = "local key = KEYS[1]\n" + "local value = ARGV[1]\n" + "local timeout = ARGV[2]\n" + "redis.call('SETEX', key, tonumber(timeout), value)\n" + "local result = redis.call('GET', key)\n" + "return result;"; private static AtomicReference<String> LUA_SHA = new AtomicReference<>(); @Test public void testLua() throws Exception { LUA_SHA.compareAndSet(null, COMMANDS.scriptLoad(RAW_LUA)); String[] keys = new String[]{"name"}; String[] args = new String[]{"throwable", "5000"}; String result = COMMANDS.evalsha(LUA_SHA.get(), ScriptOutputType.VALUE, keys, args); log.info("Get value:{}", result); } // Get value:throwable
High availability and fragmentation
For the high availability of Redis, the common master-slave mode (Master/Replica, which I call the common master-slave mode here, that is, only the master-slave replication is done, and manual switching is required for failure), sentinel and cluster are generally adopted. The normal master-slave mode can operate independently or cooperate with the sentinel, but the sentinel provides automatic failover and master node promotion functions. Ordinary master-slave and sentinel can use MasterSlave to obtain the corresponding Connection instance through the input parameters including RedisClient, codec and one or more redisuris.
Note that if the method provided in MasterSlave requires only one RedisURI instance to be passed in, then lattice will use the topology discovery mechanism to automatically obtain the Redis master-slave node information; If it is required to pass in a RedisURI collection, all node information is static for the normal master-slave mode and will not be discovered and updated.
The rules of topology discovery are as follows:
- For the normal master-slave (Master/Replica) mode, there is no need to sense whether the RedisURI points to the slave node or the master node. Only one-time topology search will be conducted to find all node information. After that, the node information will be saved in the static cache and will not be updated.
- For sentinel mode, it will subscribe to all sentinel instances and listen to subscription / publish messages to trigger topology refresh mechanism and update cached node information, that is, sentinel naturally finds node information dynamically and does not support static configuration.
The API of topology discovery mechanism is TopologyProvider. If you need to understand its principle, you can refer to the specific implementation.
For the Cluster mode, Lettuce provides a set of independent API s.
In addition, if the lattice connection is for a non single Redis node, the connection instance provides the data reading node preference (ReadFrom) setting. The optional values are:
- Master: read only from the master node.
- Master_ Err: read from Master priority node.
- SLAVE_PREFERRED: read from slave node preferentially.
- SLAVE: only read from SLAVE node.
- NEAREST: read using the Redis instance connected last time.
General master-slave mode
Suppose that three Redis services form a tree like master-slave relationship as follows:
- Node 1: localhost:6379, and the role is Master.
- Node 2: localhost:6380, the role is slave, and the slave node of node 1.
- Node 3: localhost:6381, the role is slave, and the slave node of node 2.
For the first dynamic node discovery, the node information of master-slave mode needs to be connected as follows:
@Test public void testDynamicReplica() throws Exception { // Here, you only need to configure the connection information of one node, not necessarily the information of the master node, but also the slave node RedisURI uri = RedisURI.builder().withHost("localhost").withPort(6379).build(); RedisClient redisClient = RedisClient.create(uri); StatefulRedisMasterSlaveConnection<String, String> connection = MasterSlave.connect(redisClient, new Utf8StringCodec(), uri); // Read data from nodes only connection.setReadFrom(ReadFrom.SLAVE); // Execute other Redis commands connection.close(); redisClient.shutdown(); }
If you need to specify the static Redis master-slave node connection attribute, you can build the connection as follows:
@Test public void testStaticReplica() throws Exception { List<RedisURI> uris = new ArrayList<>(); RedisURI uri1 = RedisURI.builder().withHost("localhost").withPort(6379).build(); RedisURI uri2 = RedisURI.builder().withHost("localhost").withPort(6380).build(); RedisURI uri3 = RedisURI.builder().withHost("localhost").withPort(6381).build(); uris.add(uri1); uris.add(uri2); uris.add(uri3); RedisClient redisClient = RedisClient.create(); StatefulRedisMasterSlaveConnection<String, String> connection = MasterSlave.connect(redisClient, new Utf8StringCodec(), uris); // Read data from master node only connection.setReadFrom(ReadFrom.MASTER); // Execute other Redis commands connection.close(); redisClient.shutdown(); }
Sentinel mode
Since lattice itself provides a sentinel topology discovery mechanism, you only need to configure a RedisURI instance of the sentinel node:
@Test public void testDynamicSentinel() throws Exception { RedisURI redisUri = RedisURI.builder() .withPassword("Your password") .withSentinel("localhost", 26379) .withSentinelMasterId("sentry Master of ID") .build(); RedisClient redisClient = RedisClient.create(); StatefulRedisMasterSlaveConnection<String, String> connection = MasterSlave.connect(redisClient, new Utf8StringCodec(), redisUri); // Only data is allowed to be read from the node connection.setReadFrom(ReadFrom.SLAVE); RedisCommands<String, String> command = connection.sync(); SetArgs setArgs = SetArgs.Builder.nx().ex(5); command.set("name", "throwable", setArgs); String value = command.get("name"); log.info("Get value:{}", value); } // Get value:throwable
Cluster mode
Since the author is not familiar with Redis Cluster mode, there are many restrictions on the use of API in Cluster mode, so here is only a brief introduction to how to use it. Let's start with a few features:
The following API provides the function of calling across slots:
- RedisAdvancedClusterCommands.
- RedisAdvancedClusterAsyncCommands.
- RedisAdvancedClusterReactiveCommands.
Static node selection function:
- masters: select all master nodes to execute commands.
- Slave: select all slave nodes to execute commands, which is actually read-only mode.
- All nodes: the command can be executed on all nodes.
Cluster topology view dynamic update function:
- Manually update and actively call RedisClusterClient#reloadPartitions().
- The background is updated regularly.
- Adaptive update, automatic update based on disconnection and redirection of MOVED/ASK command.
Refer to the official documents for the detailed process of setting up Redis cluster. It is assumed that the cluster has been set up as follows (192.168.56.200 is the author's virtual machine Host):
- 192.168.56.200:7001 = > master node, slot 0-5460.
- 192.168.56.200:7002 = > master node, slot 5461-10922.
- 192.168.56.200:7003 = > master node, slot 10923-16383.
- 192.168.56.200:7004 = > 7001 slave node.
- 192.168.56.200:7005 = > 7002 slave node.
- 192.168.56.200:7006 = > 7003 slave node.
Simple cluster connection and usage are as follows:
@Test public void testSyncCluster(){ RedisURI uri = RedisURI.builder().withHost("192.168.56.200").build(); RedisClusterClient redisClusterClient = RedisClusterClient.create(uri); StatefulRedisClusterConnection<String, String> connection = redisClusterClient.connect(); RedisAdvancedClusterCommands<String, String> commands = connection.sync(); commands.setex("name",10, "throwable"); String value = commands.get("name"); log.info("Get value:{}", value); } // Get value:throwable
Node selection:
@Test public void testSyncNodeSelection() { RedisURI uri = RedisURI.builder().withHost("192.168.56.200").withPort(7001).build(); RedisClusterClient redisClusterClient = RedisClusterClient.create(uri); StatefulRedisClusterConnection<String, String> connection = redisClusterClient.connect(); RedisAdvancedClusterCommands<String, String> commands = connection.sync(); // commands.all(); // All nodes // commands.masters(); // Master node // Read only from node NodeSelection<String, String> replicas = commands.slaves(); NodeSelectionCommands<String, String> nodeSelectionCommands = replicas.commands(); // This is just a demonstration. Generally, the keys * command should be disabled Executions<List<String>> keys = nodeSelectionCommands.keys("*"); keys.forEach(key -> log.info("key: {}", key)); connection.close(); redisClusterClient.shutdown(); }
Regularly update the cluster topology view (every ten minutes, which should be considered by ourselves and not too frequently):
@Test public void testPeriodicClusterTopology() throws Exception { RedisURI uri = RedisURI.builder().withHost("192.168.56.200").withPort(7001).build(); RedisClusterClient redisClusterClient = RedisClusterClient.create(uri); ClusterTopologyRefreshOptions options = ClusterTopologyRefreshOptions .builder() .enablePeriodicRefresh(Duration.of(10, ChronoUnit.MINUTES)) .build(); redisClusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(options).build()); StatefulRedisClusterConnection<String, String> connection = redisClusterClient.connect(); RedisAdvancedClusterCommands<String, String> commands = connection.sync(); commands.setex("name", 10, "throwable"); String value = commands.get("name"); log.info("Get value:{}", value); Thread.sleep(Integer.MAX_VALUE); connection.close(); redisClusterClient.shutdown(); }
Adaptively update the cluster topology view:
@Test public void testAdaptiveClusterTopology() throws Exception { RedisURI uri = RedisURI.builder().withHost("192.168.56.200").withPort(7001).build(); RedisClusterClient redisClusterClient = RedisClusterClient.create(uri); ClusterTopologyRefreshOptions options = ClusterTopologyRefreshOptions.builder() .enableAdaptiveRefreshTrigger( ClusterTopologyRefreshOptions.RefreshTrigger.MOVED_REDIRECT, ClusterTopologyRefreshOptions.RefreshTrigger.PERSISTENT_RECONNECTS ) .adaptiveRefreshTriggersTimeout(Duration.of(30, ChronoUnit.SECONDS)) .build(); redisClusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(options).build()); StatefulRedisClusterConnection<String, String> connection = redisClusterClient.connect(); RedisAdvancedClusterCommands<String, String> commands = connection.sync(); commands.setex("name", 10, "throwable"); String value = commands.get("name"); log.info("Get value:{}", value); Thread.sleep(Integer.MAX_VALUE); connection.close(); redisClusterClient.shutdown(); }
Dynamic and custom commands
Custom commands are a limited set of Redis commands, but you can specify KEY, ARGV, command type, codec and return value type in a finer granularity, depending on the dispatch() method:
// Custom implementation PING method @Test public void testCustomPing() throws Exception { RedisURI redisUri = RedisURI.builder() .withHost("localhost") .withPort(6379) .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); RedisClient redisClient = RedisClient.create(redisUri); StatefulRedisConnection<String, String> connect = redisClient.connect(); RedisCommands<String, String> sync = connect.sync(); RedisCodec<String, String> codec = StringCodec.UTF8; String result = sync.dispatch(CommandType.PING, new StatusOutput<>(codec)); log.info("PING:{}", result); connect.close(); redisClient.shutdown(); } // PING:PONG // Custom implementation Set method @Test public void testCustomSet() throws Exception { RedisURI redisUri = RedisURI.builder() .withHost("localhost") .withPort(6379) .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); RedisClient redisClient = RedisClient.create(redisUri); StatefulRedisConnection<String, String> connect = redisClient.connect(); RedisCommands<String, String> sync = connect.sync(); RedisCodec<String, String> codec = StringCodec.UTF8; sync.dispatch(CommandType.SETEX, new StatusOutput<>(codec), new CommandArgs<>(codec).addKey("name").add(5).addValue("throwable")); String result = sync.get("name"); log.info("Get value:{}", result); connect.close(); redisClient.shutdown(); } // Get value:throwable
Dynamic commands are based on the limited set of Redis commands, and some complex command combinations are realized through annotations and dynamic agents. The main comments are in io lettuce. core. dynamic. Annotation package path. For example:
public interface CustomCommand extends Commands { // SET [key] [value] @Command("SET ?0 ?1") String setKey(String key, String value); // SET [key] [value] @Command("SET :key :value") String setKeyNamed(@Param("key") String key, @Param("value") String value); // MGET [key1] [key2] @Command("MGET ?0 ?1") List<String> mGet(String key1, String key2); /** * Method name as command */ @CommandNaming(strategy = CommandNaming.Strategy.METHOD_NAME) String mSet(String key1, String value1, String key2, String value2); } @Test public void testCustomDynamicSet() throws Exception { RedisURI redisUri = RedisURI.builder() .withHost("localhost") .withPort(6379) .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); RedisClient redisClient = RedisClient.create(redisUri); StatefulRedisConnection<String, String> connect = redisClient.connect(); RedisCommandFactory commandFactory = new RedisCommandFactory(connect); CustomCommand commands = commandFactory.getCommands(CustomCommand.class); commands.setKey("name", "throwable"); commands.setKeyNamed("throwable", "doge"); log.info("MGET ===> " + commands.mGet("name", "throwable")); commands.mSet("key1", "value1","key2", "value2"); log.info("MGET ===> " + commands.mGet("key1", "key2")); connect.close(); redisClient.shutdown(); } // MGET ===> [throwable, doge] // MGET ===> [value1, value2]
Higher order characteristics
Lettue has many high-level features. Here are only two points I think are commonly used:
- Configure client resources.
- Use connection pool.
More other features can be found in the official documentation.
Configure client resources
The setting of client resources is related to the performance, concurrency and event processing of lettue. Thread pool or thread group related configurations occupy most of the client resource configuration (EventLoopGroups and eventexecutorgroups). These thread pools or thread groups are the basic components of the connector. In general, client resources should be shared among multiple Redis clients, and they need to be shut down when they are no longer in use. The author believes that client resources are Netty oriented. Note: < font color = Red > unless you are particularly familiar with or spend a long time testing and adjusting the parameters mentioned below, you may step on the pit if you change the default value intuitively without experience.
The client resource interface is ClientResources, and the implementation class is DefaultClientResources.
Build DefaultClientResources instance:
// default ClientResources resources = DefaultClientResources.create(); // Builder ClientResources resources = DefaultClientResources.builder() .ioThreadPoolSize(4) .computationThreadPoolSize(4) .build()
use:
ClientResources resources = DefaultClientResources.create(); // Non cluster RedisClient client = RedisClient.create(resources, uri); // colony RedisClusterClient clusterClient = RedisClusterClient.create(resources, uris); // ...... client.shutdown(); clusterClient.shutdown(); // close resource resources.shutdown();
Basic configuration of client resources:
attribute | describe | Default value |
---|---|---|
ioThreadPoolSize | I/O threads | Runtime.getRuntime().availableProcessors() |
computationThreadPoolSize | Number of task threads | Runtime.getRuntime().availableProcessors() |
Advanced configuration of client resources:
attribute | describe | Default value |
---|---|---|
eventLoopGroupProvider | EventLoopGroup provider | - |
eventExecutorGroupProvider | EventExecutorGroup provider | - |
eventBus | Event bus | DefaultEventBus |
commandLatencyCollectorOptions | Command delay collector configuration | DefaultCommandLatencyCollectorOptions |
commandLatencyCollector | Command delay collector | DefaultCommandLatencyCollector |
commandLatencyPublisherOptions | Command delay publisher configuration | DefaultEventPublisherOptions |
dnsResolver | DNS processor | Provided by JDK or Netty |
reconnectDelay | Reconnection delay configuration | Delay.exponential() |
nettyCustomizer | Netty custom Configurator | - |
tracing | Track recorder | - |
Attribute configuration of non cluster client RedisClient:
Redis non cluster client RedisClient itself provides the method of configuring attributes:
RedisClient client = RedisClient.create(uri); client.setOptions(ClientOptions.builder() .autoReconnect(false) .pingBeforeActivateConnection(true) .build());
List of configuration properties of non cluster clients:
attribute | describe | Default value |
---|---|---|
pingBeforeActivateConnection | Whether to execute PING command before connection activation | false |
autoReconnect | Automatic reconnection | true |
cancelCommandsOnReconnectFailure | Reconnection failed. Reject command execution | false |
suspendReconnectOnProtocolFailure | Underlying protocol failed. Suspend reconnection | false |
requestQueueSize | Request queue capacity | 2147483647(Integer#MAX_VALUE) |
disconnectedBehavior | Behavior when losing connection | DEFAULT |
sslOptions | SSL configuration | - |
socketOptions | Socket configuration | 10 seconds Connection-Timeout, no keep-alive, no TCP noDelay |
timeoutOptions | Timeout configuration | - |
publishOnScheduler | Scheduler for publishing reactive signal data | Using I/O threads |
Cluster client attribute configuration:
Redis cluster client RedisClusterClient itself provides the method of configuring attributes:
RedisClusterClient client = RedisClusterClient.create(uri); ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() .enablePeriodicRefresh(refreshPeriod(10, TimeUnit.MINUTES)) .enableAllAdaptiveRefreshTriggers() .build(); client.setOptions(ClusterClientOptions.builder() .topologyRefreshOptions(topologyRefreshOptions) .build());
List of configuration attributes of cluster client:
attribute | describe | Default value |
---|---|---|
enablePeriodicRefresh | Whether to allow periodic updating of cluster topology view | false |
refreshPeriod | Update cluster topology view cycle | 60 seconds |
enableAdaptiveRefreshTrigger | Set the adaptive update cluster topology view trigger RefreshTrigger | - |
adaptiveRefreshTriggersTimeout | Adaptive update cluster topology view trigger timeout setting | 30 seconds |
refreshTriggersReconnectAttempts | Number of reconnections triggered by adaptively updating the cluster topology view | 5 |
dynamicRefreshSources | Allow dynamic refresh of topology resources | true |
closeStaleConnections | Allow stale connections to be closed | true |
maxRedirects | Maximum number of cluster redirects | 5 |
validateClusterNodeMembership | Verify the membership of cluster nodes | true |
Use connection pool
Introducing connection pool dependency commons-pool2:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.7.0</version> </dependency
The basic usage is as follows:
@Test public void testUseConnectionPool() throws Exception { RedisURI redisUri = RedisURI.builder() .withHost("localhost") .withPort(6379) .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); RedisClient redisClient = RedisClient.create(redisUri); GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); GenericObjectPool<StatefulRedisConnection<String, String>> pool = ConnectionPoolSupport.createGenericObjectPool(redisClient::connect, poolConfig); try (StatefulRedisConnection<String, String> connection = pool.borrowObject()) { RedisCommands<String, String> command = connection.sync(); SetArgs setArgs = SetArgs.Builder.nx().ex(5); command.set("name", "throwable", setArgs); String n = command.get("name"); log.info("Get value:{}", n); } pool.close(); redisClient.shutdown(); }
Among them, ConnectionPoolSupport is required for the pooling support of synchronous connections, and AsyncConnectionPoolSupport is required for the pooling support of asynchronous connections (only supported after lettue5.1).
Several common examples of progressive deletion
Gradually delete domain attributes in Hash:
@Test public void testDelBigHashKey() throws Exception { // SCAN parameters ScanArgs scanArgs = ScanArgs.Builder.limit(2); // TEMP cursor ScanCursor cursor = ScanCursor.INITIAL; // Target KEY String key = "BIG_HASH_KEY"; prepareHashTestData(key); log.info("Start progressive deletion Hash Element of..."); int counter = 0; do { MapScanCursor<String, String> result = COMMAND.hscan(key, cursor, scanArgs); // Reset TEMP cursor cursor = ScanCursor.of(result.getCursor()); cursor.setFinished(result.isFinished()); Collection<String> fields = result.getMap().values(); if (!fields.isEmpty()) { COMMAND.hdel(key, fields.toArray(new String[0])); } counter++; } while (!(ScanCursor.FINISHED.getCursor().equals(cursor.getCursor()) && ScanCursor.FINISHED.isFinished() == cursor.isFinished())); log.info("Progressive deletion Hash Complete element,Number of iterations:{} ...", counter); } private void prepareHashTestData(String key) throws Exception { COMMAND.hset(key, "1", "1"); COMMAND.hset(key, "2", "2"); COMMAND.hset(key, "3", "3"); COMMAND.hset(key, "4", "4"); COMMAND.hset(key, "5", "5"); }
Progressive deletion of elements in the collection:
@Test public void testDelBigSetKey() throws Exception { String key = "BIG_SET_KEY"; prepareSetTestData(key); // SCAN parameters ScanArgs scanArgs = ScanArgs.Builder.limit(2); // TEMP cursor ScanCursor cursor = ScanCursor.INITIAL; log.info("Start progressive deletion Set Element of..."); int counter = 0; do { ValueScanCursor<String> result = COMMAND.sscan(key, cursor, scanArgs); // Reset TEMP cursor cursor = ScanCursor.of(result.getCursor()); cursor.setFinished(result.isFinished()); List<String> values = result.getValues(); if (!values.isEmpty()) { COMMAND.srem(key, values.toArray(new String[0])); } counter++; } while (!(ScanCursor.FINISHED.getCursor().equals(cursor.getCursor()) && ScanCursor.FINISHED.isFinished() == cursor.isFinished())); log.info("Progressive deletion Set Complete element,Number of iterations:{} ...", counter); } private void prepareSetTestData(String key) throws Exception { COMMAND.sadd(key, "1", "2", "3", "4", "5"); }
Progressive deletion of elements in an ordered collection:
@Test public void testDelBigZSetKey() throws Exception { // SCAN parameters ScanArgs scanArgs = ScanArgs.Builder.limit(2); // TEMP cursor ScanCursor cursor = ScanCursor.INITIAL; // Target KEY String key = "BIG_ZSET_KEY"; prepareZSetTestData(key); log.info("Start progressive deletion ZSet Element of..."); int counter = 0; do { ScoredValueScanCursor<String> result = COMMAND.zscan(key, cursor, scanArgs); // Reset TEMP cursor cursor = ScanCursor.of(result.getCursor()); cursor.setFinished(result.isFinished()); List<ScoredValue<String>> scoredValues = result.getValues(); if (!scoredValues.isEmpty()) { COMMAND.zrem(key, scoredValues.stream().map(ScoredValue<String>::getValue).toArray(String[]::new)); } counter++; } while (!(ScanCursor.FINISHED.getCursor().equals(cursor.getCursor()) && ScanCursor.FINISHED.isFinished() == cursor.isFinished())); log.info("Progressive deletion ZSet Complete element,Number of iterations:{} ...", counter); } private void prepareZSetTestData(String key) throws Exception { COMMAND.zadd(key, 0, "1"); COMMAND.zadd(key, 0, "2"); COMMAND.zadd(key, 0, "3"); COMMAND.zadd(key, 0, "4"); COMMAND.zadd(key, 0, "5"); }
Using Lettuce in SpringBoot
Personally, I don't think the API encapsulation in spring data redis is very good. It's heavy to use and not flexible enough. Here, combined with the previous examples and codes, configure and integrate Lettuce in the SpringBoot scaffold project. Import dependency first:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.1.8.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>5.1.8.RELEASE</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency> </dependencies>
Generally, each application should use a single Redis client instance and a single connection instance. Here, a scaffold is designed to adapt to four use scenarios: single machine, ordinary master-slave, sentry and cluster. For client resources, the default implementation can be adopted. For Redis connection properties, the main ones are Host, Port and Password. Others can be ignored temporarily. Based on the principle that convention is greater than configuration, customize a series of attribute configuration classes first (in fact, some configurations can be completely shared, but considering the relationship between classes to be clearly described, multiple configuration attribute classes and multiple configuration methods are split here):
@Data @ConfigurationProperties(prefix = "lettuce") public class LettuceProperties { private LettuceSingleProperties single; private LettuceReplicaProperties replica; private LettuceSentinelProperties sentinel; private LettuceClusterProperties cluster; } @Data public class LettuceSingleProperties { private String host; private Integer port; private String password; } @EqualsAndHashCode(callSuper = true) @Data public class LettuceReplicaProperties extends LettuceSingleProperties { } @EqualsAndHashCode(callSuper = true) @Data public class LettuceSentinelProperties extends LettuceSingleProperties { private String masterId; } @EqualsAndHashCode(callSuper = true) @Data public class LettuceClusterProperties extends LettuceSingleProperties { }
The configuration classes are as follows, mainly using @ ConditionalOnProperty for isolation. Generally, few people will use more than one Redis connection scenario in an application:
@RequiredArgsConstructor @Configuration @ConditionalOnClass(name = "io.lettuce.core.RedisURI") @EnableConfigurationProperties(value = LettuceProperties.class) public class LettuceAutoConfiguration { private final LettuceProperties lettuceProperties; @Bean(destroyMethod = "shutdown") public ClientResources clientResources() { return DefaultClientResources.create(); } @Bean @ConditionalOnProperty(name = "lettuce.single.host") public RedisURI singleRedisUri() { LettuceSingleProperties singleProperties = lettuceProperties.getSingle(); return RedisURI.builder() .withHost(singleProperties.getHost()) .withPort(singleProperties.getPort()) .withPassword(singleProperties.getPassword()) .build(); } @Bean(destroyMethod = "shutdown") @ConditionalOnProperty(name = "lettuce.single.host") public RedisClient singleRedisClient(ClientResources clientResources, @Qualifier("singleRedisUri") RedisURI redisUri) { return RedisClient.create(clientResources, redisUri); } @Bean(destroyMethod = "close") @ConditionalOnProperty(name = "lettuce.single.host") public StatefulRedisConnection<String, String> singleRedisConnection(@Qualifier("singleRedisClient") RedisClient singleRedisClient) { return singleRedisClient.connect(); } @Bean @ConditionalOnProperty(name = "lettuce.replica.host") public RedisURI replicaRedisUri() { LettuceReplicaProperties replicaProperties = lettuceProperties.getReplica(); return RedisURI.builder() .withHost(replicaProperties.getHost()) .withPort(replicaProperties.getPort()) .withPassword(replicaProperties.getPassword()) .build(); } @Bean(destroyMethod = "shutdown") @ConditionalOnProperty(name = "lettuce.replica.host") public RedisClient replicaRedisClient(ClientResources clientResources, @Qualifier("replicaRedisUri") RedisURI redisUri) { return RedisClient.create(clientResources, redisUri); } @Bean(destroyMethod = "close") @ConditionalOnProperty(name = "lettuce.replica.host") public StatefulRedisMasterSlaveConnection<String, String> replicaRedisConnection(@Qualifier("replicaRedisClient") RedisClient replicaRedisClient, @Qualifier("replicaRedisUri") RedisURI redisUri) { return MasterSlave.connect(replicaRedisClient, new Utf8StringCodec(), redisUri); } @Bean @ConditionalOnProperty(name = "lettuce.sentinel.host") public RedisURI sentinelRedisUri() { LettuceSentinelProperties sentinelProperties = lettuceProperties.getSentinel(); return RedisURI.builder() .withPassword(sentinelProperties.getPassword()) .withSentinel(sentinelProperties.getHost(), sentinelProperties.getPort()) .withSentinelMasterId(sentinelProperties.getMasterId()) .build(); } @Bean(destroyMethod = "shutdown") @ConditionalOnProperty(name = "lettuce.sentinel.host") public RedisClient sentinelRedisClient(ClientResources clientResources, @Qualifier("sentinelRedisUri") RedisURI redisUri) { return RedisClient.create(clientResources, redisUri); } @Bean(destroyMethod = "close") @ConditionalOnProperty(name = "lettuce.sentinel.host") public StatefulRedisMasterSlaveConnection<String, String> sentinelRedisConnection(@Qualifier("sentinelRedisClient") RedisClient sentinelRedisClient, @Qualifier("sentinelRedisUri") RedisURI redisUri) { return MasterSlave.connect(sentinelRedisClient, new Utf8StringCodec(), redisUri); } @Bean @ConditionalOnProperty(name = "lettuce.cluster.host") public RedisURI clusterRedisUri() { LettuceClusterProperties clusterProperties = lettuceProperties.getCluster(); return RedisURI.builder() .withHost(clusterProperties.getHost()) .withPort(clusterProperties.getPort()) .withPassword(clusterProperties.getPassword()) .build(); } @Bean(destroyMethod = "shutdown") @ConditionalOnProperty(name = "lettuce.cluster.host") public RedisClusterClient redisClusterClient(ClientResources clientResources, @Qualifier("clusterRedisUri") RedisURI redisUri) { return RedisClusterClient.create(clientResources, redisUri); } @Bean(destroyMethod = "close") @ConditionalOnProperty(name = "lettuce.cluster") public StatefulRedisClusterConnection<String, String> clusterConnection(RedisClusterClient clusterClient) { return clusterClient.connect(); } }
Finally, in order for the IDE to recognize our configuration, you can add ide affinity, and add a file spring configuration metadata. In the / META-INF folder JSON, as follows:
{ "properties": [ { "name": "lettuce.single", "type": "club.throwable.spring.lettuce.LettuceSingleProperties", "description": "Stand alone configuration", "sourceType": "club.throwable.spring.lettuce.LettuceProperties" }, { "name": "lettuce.replica", "type": "club.throwable.spring.lettuce.LettuceReplicaProperties", "description": "Master slave configuration", "sourceType": "club.throwable.spring.lettuce.LettuceProperties" }, { "name": "lettuce.sentinel", "type": "club.throwable.spring.lettuce.LettuceSentinelProperties", "description": "Sentry configuration", "sourceType": "club.throwable.spring.lettuce.LettuceProperties" }, { "name": "lettuce.single", "type": "club.throwable.spring.lettuce.LettuceClusterProperties", "description": "Cluster configuration", "sourceType": "club.throwable.spring.lettuce.LettuceProperties" } ] }
If you want to make IDE affinity better, you can add / meta-inf / additional spring configuration metadata JSON for more detailed definition. Simple use is as follows:
@Slf4j @Component public class RedisCommandLineRunner implements CommandLineRunner { @Autowired @Qualifier("singleRedisConnection") private StatefulRedisConnection<String, String> connection; @Override public void run(String... args) throws Exception { RedisCommands<String, String> redisCommands = connection.sync(); redisCommands.setex("name", 5, "throwable"); log.info("Get value:{}", redisCommands.get("name")); } } // Get value:throwable
Summary
This paper is based on the official document of Lettuce, and makes a comprehensive analysis of its use, including some examples of main functions and configuration. Due to space limitation, some features and configuration details are not analyzed. Lattice has been accepted by spring data Redis as the official Redis client driver, so it is trustworthy. Some of its API designs are indeed reasonable, with high scalability and flexibility. Personally, it's easy to add configuration to SpringBoot application based on lattice package. After all, RedisTemplate is too cumbersome, and it also shields some advanced features and flexible APIs of lattice.
reference material:
link
- Github Page: http://www.throwable.club/2019/09/28/redis-client-driver-lettuce-usage
- Coding Page: http://throwable.coding.me/2019/09/28/redis-client-driver-lettuce-usage
(end of this article c-14-d e-a-20190928 too many things have happened recently...)