10
头图

1. What is Lettuce?

At a technical seminar, everyone talked about which Redis Java client is strong, and I immediately shouted "Jedis, YES!" without hesitation.

"Jedis is the official client. It's easy to use. The company's middleware uses it. Is there a second player besides Jedis?" I directly threw Wang Bo.

Xiao Zhang, who just learned Spring, was dissatisfied: "SpringDataRedis uses RedisTemplate! Jedis? Doesn't exist."

"Sit down, Xiu'er, SpringDataRedis is based on Jedis encapsulation." Next to him, Li Ge took a sip of happy water, the corners of his mouth rose slightly, revealing a trace of disdain.

"A lot of Lettuce are used now, don't you know?" Lao Wang pushed his glasses and said faintly, then slowly opened the windows of the soul behind the lenses, and looked down at us with a caring look. .

Lettuce? lettuce? Confused, I quickly opened the client list on the Redis official website. Found that there are three officially recommended implementations of the Java language: Jedis , Lettuce and Redission .

(Source of screenshot: https://redis.io/clients#java )

What client is Lettuce? Have not heard. But it was found that its official introduction was the longest:

Advanced Redis client for thread-safe sync, async, and reactive usage. Supports Cluster, Sentinel, Pipelining, and codecs.

Quickly look up the dictionary and translate it:

  • Advanced client
  • Thread safe
  • Support synchronous, asynchronous and reactive API
  • Support cluster, sentinel, pipeline and codec

Lao Wang waved his hand to sign me to put away the dictionary, and introduced it slowly.

1.1 Advanced Client

"Master, do you translate for the translator, what (beep——) is called (beep——) advanced client?"

"Advanced client, advanced, it is Advanced! New can be used immediately, no need to worry about the implementation details, just pick up the business logic directly."

1.2 Thread safety

This is one of the main differences from Jedis.

Jedis's connection instance is thread-unsafe, so a connection pool needs to be maintained. Each thread takes out the connection instance from the connection pool when needed, and returns the instance after completing the operation or encountering an exception. When the number of connections continues to rise with the business, the consumption of physical connections will also become a potential risk point for performance and stability.

Lettuce uses Netty as a communication layer component, and its connection instance is thread-safe, and when conditions are met, it can access the operating system's native call epoll, kqueue, etc. to obtain performance improvements.

We know that although the Redis server instance can connect to multiple clients to send and receive commands at the same time, each instance is single-threaded when executing commands.

This means that if an application can operate Redis through multithreading + single connection, it will be able to streamline the total number of connections on the Redis server, and it will be possible to obtain better stability and performance when multiple applications share the same Redis server. For applications, it also reduces the resource consumption of maintaining multiple connection instances.

1.3 Support synchronous, asynchronous and reactive API

Lettuce has been designed in accordance with non-blocking IO from the beginning. It is a purely asynchronous client with comprehensive support for asynchronous and reactive APIs.

Even if it is a synchronous command, the underlying communication process is still an asynchronous model, which just simulates the synchronization effect by blocking the calling thread.

1.4 Support cluster, sentinel, pipeline and codec

"These features are standard. Lettuce is an advanced client! Advanced, understand?" Lao Wang said that he was excitedly pointing his finger on the desktop, but he didn't seem to want to introduce more. I silently noted that I planned to learn it. .

(During the use of the project, the pipeling mechanism is slightly more abstract than Jedis. The pits and solutions encountered during use will be given below.)

1.5 Usage in Spring

In addition to the official introduction of Redis, we can also find that Spring Data Redis upgraded Lettuce to 5.0 when it was upgraded to 2.0. In fact, Lettuce has been officially integrated in SpringDataRedis 1.6; SpringSessionDataRedis directly uses Lettuce as the default Redis client, which shows its maturity and stability.

Jedis is widely known and even a de-facto standard driver, and it was launched early (version 1.0.0 in September 2010, Lettuce 1.0.0 is in March 2011), the API is straightforward and easy to use, Features such as the fastest support for new Redis features are inseparable.

But lettuce is a latecomer, and its advantages and ease of use have also won the favor of Spring and other communities. The following will share our experience summary when integrating Lettuce in the project for your reference.

2. What are the main differences between Jedis and Lettuce?

Having said that, what are the main differences between Lettuce and the old client Jedis? We can look at the comparison table given in the Spring Data Redis help document:

(Screenshot source: https://docs.spring.io )

Note: The X mark is support.

After comparison, we can find:

  • All Lettuce supported by Jedis are supported;
  • Lettuce, which is not supported by Jedis, is also supported!

So it is not surprising that Lettuce is used more and more in Spring.

Three, Lettuce first experience

Just talk about it, let’s share with you what we gained when we tried Lettuce, especially the batch command part that took a lot of time to step on the pit, which will be explained in detail below.

3.1 Quick start

If the simplest examples are puzzling, then this library will certainly not become popular. Lettuce's quick start is really fast:

a. Introduce maven dependencies (other dependencies are similar, see the reference at the end of the article for details)

<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>5.3.6.RELEASE</version>
</dependency>

b. Fill in the Redis address, connect, execute, and close. Perfect!

import io.lettuce.core.*;
 
// Syntax: redis://[password@]host[:port][/databaseNumber]
// Syntax: redis://[username:password@]host[:port][/databaseNumber]
RedisClient redisClient = RedisClient.create("redis://password@localhost:6379/0");
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisCommands<String, String> syncCommands = connection.sync();
 
syncCommands.set("key", "Hello, Redis!");
 
connection.close();
redisClient.shutdown();

3.2 Does cluster mode support? stand by!

Redis Cluster is the official Redis Sharding solution. You should be very familiar with it and no longer introduce it. The official document can refer to Redis Cluster 101 .

Lettuce connect to the Redis cluster and change the above client code line:

// Syntax: redis://[password@]host[:port]
// Syntax: redis://[username:password@]host[:port]
RedisClusterClient redisClient = RedisClusterClient.create("redis://password@localhost:7379");

3.3 Does it support high reliability? stand by!

Redis Sentinel is an official high-reliability solution. Sentinel can automatically switch to the slave node to continue to provide services when an instance fails. For official documents, please refer to Redis Sentinel Documentation .

It is still enough to replace the client's creation method:

// Syntax: redis-sentinel://[password@]host[:port][,host2[:port2]][/databaseNumber]#sentinelMasterId
RedisClient redisClient = RedisClient.create("redis-sentinel://localhost:26379,localhost:26380/0#mymaster");

3.4 Does the pipeline under the cluster support? stand by!

Although Jedis has pipeline commands, it cannot support Redis Cluster. Generally, you need to merge the slots and instances where each key is located by yourself, and then execute the pipeline in batches.

The official website supports PR for the pipeline under the cluster. As of the writing of this article (February 2021), four years have passed and it has not yet been . It Cluster pipelining 160ecf3b2e2a98.

Although Lettuce claims to support pipeling, it does not directly see the pipeline API. What is going on?

3.4.1 Implementation of pipeline

Use AsyncCommands and flushCommands to implement pipelines. After reading official documents, you can know that Lettuce's synchronous and asynchronous commands actually share the same connection instance, and the bottom layer uses pipelines to send/receive commands.

The difference is that:

From this we can implement the pipeline through asynchronous commands + manual batch push, look at the official example :

StatefulRedisConnection<String, String> connection = client.connect();
RedisAsyncCommands<String, String> commands = connection.async();
 
// disable auto-flushing
commands.setAutoFlushCommands(false);
 
// perform a series of independent calls
List<RedisFuture<?>> futures = Lists.newArrayList();
for (int i = 0; i < iterations; i++) {
futures.add(commands.set("key-" + i, "value-" + i));
futures.add(commands.expire("key-" + i, 3600));
}
 
// write all commands to the transport layer
commands.flushCommands();
 
// synchronization example: Wait until all futures complete
boolean result = LettuceFutures.awaitAll(5, TimeUnit.SECONDS,
futures.toArray(new RedisFuture[futures.size()]));
 
// later
connection.close();

3.4.2 Is there any problem with this?

It looks perfect at first glance, but in fact there is a pit: setAutoFlushCommands(false) After setting, you will find that the synchronization commands called by the sync() method are not returned! Why is this? Let's take a look at the official documents:

Lettuce is a non-blocking and asynchronous client. It provides a synchronous API to achieve a blocking behavior on a per-Thread basis to create await (synchronize) a command response..... As soon as the first request returns, the first Thread’s program flow continues, while the second request is processed by Redis and comes back at a certain point in time

Sync and async are the same in the underlying implementation, but sync simulates the synchronization operation by blocking the calling thread. And setAutoFlushCommands can be found through the source code that it acts on the connection object, so this operation is effective for both sync and async command objects.

Therefore, as long as auto flush commands is set to false in a thread, it will affect all other threads that use the connection instance.

/**
* An asynchronous and thread-safe API for a Redis connection.
*
* @param <K> Key type.
* @param <V> Value type.
* @author Will Glozer
* @author Mark Paluch
*/
public abstract class AbstractRedisAsyncCommands<K, V> implements RedisHashAsyncCommands<K, V>, RedisKeyAsyncCommands<K, V>,
RedisStringAsyncCommands<K, V>, RedisListAsyncCommands<K, V>, RedisSetAsyncCommands<K, V>,
RedisSortedSetAsyncCommands<K, V>, RedisScriptingAsyncCommands<K, V>, RedisServerAsyncCommands<K, V>,
RedisHLLAsyncCommands<K, V>, BaseRedisAsyncCommands<K, V>, RedisTransactionalAsyncCommands<K, V>,
RedisGeoAsyncCommands<K, V>, RedisClusterAsyncCommands<K, V> {
    @Override
    public void setAutoFlushCommands(boolean autoFlush) {
        connection.setAutoFlushCommands(autoFlush);
    }
}

Correspondingly, if multiple threads call async() to obtain the asynchronous command set, and call flushCommands() after their own business logic is completed, they will be forced to flush the asynchronous commands that other threads are still appending, which logically belong to the entire batch of commands. Will be broken up into multiple copies and sent.

Although it does not affect the correctness of the result, if the commands to disperse each other because of the mutual influence of threads are sent, the performance improvement will be very unstable.

Naturally we will think: each batch command creates a connection, and then... Doesn't this also rely on the connection pool like Jedis?

Recalling the soul-piercing gaze behind Lao Wang's lens, I plan to bite the bullet and dig again. Sure enough, after reading the document carefully again, I found another good thing: Batch Execution .

3.4.3 Batch Execution

Since flushCommands will have a global impact on the connection, shouldn't flush be limited to the thread level? I found the official example from the documentation.

Recalling the previous article Lettuce is an advanced client. After reading the document, I found that it is indeed advanced. You only need to define the interface (reminiscent of the Mapper interface of MyBatis). The following is an example used in the project:

/
/**
 * 定义会用到的批量命令
 */
@BatchSize(100)
public interface RedisBatchQuery extends Commands, BatchExecutor {
    RedisFuture<byte[]> get(byte[] key);
    RedisFuture<Set<byte[]>> smembers(byte[] key);
    RedisFuture<List<byte[]>> lrange(byte[] key, long start, long end);
    RedisFuture<Map<byte[], byte[]>> hgetall(byte[] key);
}

Operate like this when calling:

// 创建客户端
RedisClusterClient client = RedisClusterClient.create(DefaultClientResources.create(), "redis://" + address);
 
// service 中持有 factory 实例,只创建一次。第二个参数表示 key 和 value 使用 byte[] 编解码
RedisCommandFactory factory = new RedisCommandFactory(connect, Arrays.asList(ByteArrayCodec.INSTANCE, ByteArrayCodec.INSTANCE));
 
// 使用的地方,创建一个查询实例代理类调用命令,最后刷入命令
List<RedisFuture<?>> futures = new ArrayList<>();
RedisBatchQuery batchQuery = factory.getCommands(RedisBatchQuery.class);
for (RedisMetaGroup redisMetaGroup : redisMetaGroups) {
    // 业务逻辑,循环调用多个 key 并将结果保存到 futures 结果中
    appendCommand(redisMetaGroup, futures, batchQuery);
}
 
// 异步命令调用完成后执行 flush 批量执行,此时命令才会发送给 Redis 服务端
batchQuery.flush();

It's that simple.

At this time, the batch control will be performed at the thread granularity, and the batch operation will be executed when flush is called or the number of cache commands configured by @BatchSize is reached. For the connection instance, you don't need to set auto flush commands anymore, just keep the default true, which will not affect other threads.

ps: Excellent and rigorous, you will definitely think: if a single command takes a long time to execute or someone puts a command such as BLPOP, it will definitely have an impact. This topic is also covered in official documents, and you can consider using connection pooling.

3.5 Can you give me more power?

Of course, Lettuce supports not only the simple functions mentioned above, but also these are also worth trying:

3.5.1 Separation of Read and Write

We know that Redis instances support master-slave deployment. The slave instances synchronize data from the master instance asynchronously, and use Redis Sentinel to perform master-slave switching when the master instance fails.

When the application is not sensitive to data consistency and requires greater throughput, the master-slave read-write separation method can be considered. Lettuce can set the readFrom configuration of StatefulRedisClusterConnection to adjust:

3.5.2 Configuring automatic update of cluster topology

When using Redis Cluster, what should I do if the server is expanded?

Lettuce has long considered it-pass in the ClusterClientOptions object through the RedisClusterClient#setOptions method to configure the relevant parameters (see the reference link at the end of the article for all configurations).

Common configurations of topologyRefreshOptions in ClusterClientOptions are as follows:

3.5.3 Connection Pool

Although Lettuce's thread-safe single-connection instances already have very good performance, it does not rule out that some large businesses need to use thread pools to improve throughput. In addition, it is necessary to exclusively connect for transactional operations.

Lettuce provides connection pooling capabilities based on the Apache Common-pool2 component (the following is an example of the use of the client thread pool corresponding to the official RedisCluster):

RedisClusterClient clusterClient = RedisClusterClient.create(RedisURI.create(host, port));
 
GenericObjectPool<StatefulRedisClusterConnection<String, String>> pool = ConnectionPoolSupport
               .createGenericObjectPool(() -> clusterClient.connect(), new GenericObjectPoolConfig());
 
// execute work
try (StatefulRedisClusterConnection<String, String> connection = pool.borrowObject()) {
    connection.sync().set("key", "value");
    connection.sync().blpop(10, "list");
}
 
// terminating
pool.close();
clusterClient.shutdown();

What needs to be explained here is: createGenericObjectPool creates a connection pool and sets the wrapConnections parameter to true by default. The close method of the loaned object at this time will be reloaded as a return connection through a dynamic proxy; if it is set to false, the close method will close the connection.

Lettuce also supports asynchronous connection pool (getting a connection from the connection pool is an asynchronous operation), please refer to the link at the end of the article for details. There are still many features that cannot be listed one by one. You can find instructions and examples in the official documentation, which is worth reading.

Four, use summary

Compared with Jedis, Lettuce is more convenient and faster to use, and has a high degree of abstraction. And through thread-safe connections, the number of connections in the system is reduced, and the stability of the system is improved.

For advanced players, Lettuce also provides many configurations and interfaces to facilitate performance optimization and in-depth business customization scenarios.

Another point I have to say is that Lettuce's official document is very comprehensive and detailed, which is very rare. The community is relatively active, and the Commiter will actively answer various issues, which allows many questions to be resolved by themselves.

In contrast, Jedis's documentation, maintenance and update speed are relatively slow. PR JedisCluster pipeline has not been integrated in the past four years (February 2021).

Reference

Two of the GitHub issues contain high gold content, and I highly recommend reading them!

1. Lettuce quick start: https://lettuce.io

2.Redis Java Clients

3. Lettuce official website: https://lettuce.io

4.SpringDataRedis reference document

5.Question about pipelining

6.Why is Lettuce the default Redis client used in Spring Session Redis

7.Cluster-specific options:https://lettuce.io

8. Lettuce connection pool

9. Client configuration: https://lettuce.io/core/release

10.SSL configuration: https://lettuce.io

Author: vivo Internet Data Intelligence Team-Li Haoxuan

vivo互联网技术
3.3k 声望10.2k 粉丝