1. What is Lettuce?

During a technical conference, when people started talking about the Java client of Redis, I immediately shouted out “Jedis, YES! “without hesitation.

“Jedis is the official client, so it’s a no-nonsense way to use it. Companies use it in their middleware. Is there a second player besides Jedis?” I just throw the king fried.

Xiao Zhang, who just learned Spring, said: “Spring dataredis all use RedisTemplate! Jedis? It doesn’t exist.”

“Sit down, Shul, SpringDataRedis is based on Jedis.” Flank li elder brother sipped one mouthful just open happy water, the corners of the mouth slightly up, peep out a trace of disdain.

“A lot of Lettuce is used now, don’t you know? Lao Wang pushed his glasses lightly to say, then slowly open the window of the heart after the lens, with caring eyes looking down at us a few vegetables chicken.

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

(Screenshot: https://redis.io/clients#java)

What client is Lettuce? Haven’t heard. But found its official introduction to be the longest:

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

I quickly looked up the dictionary and translated it:

  • Advanced Client
  • Thread safety
  • Supports synchronous, asynchronous, and reactive APIs
  • Support for clustering, sentinel, piping, and codec

Lao Wang beckoned me to put away the dictionary and introduced it slowly.

1.1 Advanced client

“Master, you translate for the translator, what beep –) is called beep –) advanced client?”

“Advanced client, Advanced client, Advanced client. “New, no implementation details, just pick up the business logic and go ahead.”

1.2 Thread safety

This is one of the main differences with Jedis.

The connection instances of Jedis are thread-unsafe, so a connection pool needs to be maintained. Each thread needs to remove the connection instances from the connection pool, and return the connection instances after the completion of the operation or when it encounters an exception. As the number of connections continues to rise with the business, the consumption of physical connections can also become a potential point of risk to performance and stability.

Lettuce uses Netty as a communication layer component, its connection instances are thread-safe, and when conditions are available, it can access the operating system to call epoll, kqueue and so on to obtain performance improvement.

We know that while the Redis server instance can connect to multiple clients to send and receive commands simultaneously, each instance executes commands in a single thread.

This means that if applications can operate Redis with multiple threads and single connections, the total number of connections to the Redis server can be reduced, and more stability and performance can be achieved when multiple applications share the same Redis server. It also reduces the cost of maintaining multiple connection instances for the application.

1.3 Support for synchronous, asynchronous, and reactive APIs

Designed from the outset with non-blocking IO, Lettuce is a purely asynchronous client with comprehensive support for both asynchronous and reactive APIs.

Even if the command is synchronous, the underlying communication process is still asynchronous, only by blocking the calling thread to simulate the effect of synchronization.

1.4 Support for cluster, sentinel, pipe and codec

“These features are standard, Lettuce is a premium client! Advanced, understand?” Lao Wang said here excitedly pointed to the table with his finger, but it seemed that he did not want to do more introducations, I silently wrote down the plan to learn a good.

(During the project, the pipeling mechanism is a little more abstract than the Jedis mechanism. The following sections describe the pits encountered during the use and the solutions.)

1.5 Usage in Spring

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

Jedis is a well-known, and de facto, standard Java client (De-Facto Standard Driver), and it was introduced earlier (version 1.0.0 September 2010). Lettuce 1.0.0 March 2011), the API is direct and easy to use, and the fastest support for new Redis features are inseparable.

However, as a late comer, Lettuce has gained favor from communities such as Spring with its advantages and ease of use. We will share the summary of our experience in 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: https://docs.spring.io)

Note: where X is marked for support.

After comparison, we can find that:

  • Lettuce supported by Jedis;
  • What Jedis does not support, Lettuce does!

So it is not surprising that Spring is increasingly used in Lettuce.

Three, early experience

Let us share what we have learned when we try Lettuce, especially the batch command part, which takes a lot of time to trave, more details below.

3.1 Quick Start

If even the simplest examples are confusing, the library will not catch on. Well, Lettuce has a fast start really fast:

A. Introduce Maven dependencies (other dependencies are similar, see Resources at the end of this article)

< the dependency > < groupId > IO. Lettuce < / groupId > < artifactId > lettuce - core < / artifactId > < version > 5.3.6. RELEASE < / version > </dependency>

B. Enter the Redis address, connect, execute, 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 clustering mode support? Support!

Redis Cluster is the official Redis Sharding solution. You should be familiar with it without further introduction. The official documentation can be found in Redis Cluster 101.

A line of the above client code can be changed:

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

3.3 Is the support highly reliable? Support!

Redis Sentinel is a highly reliable solution provided by the government, which can automatically switch to slave nodes to continue service in the event of an instance failure. The official Documentation can be found on Redis Sentinel Documentation.

Again, just replace the way the client was created:

// 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 cluster-based Pipeline support? Support!

Jedis does have the PIPELINE command but cannot support Redis Cluster. In general, it is necessary to merge the slot and instance of each key before executing the pipeline in batch.

As of February 2021, four years have passed and the pipeline support PR under the Cluster has still not been integrated.

Well, Lettuce claims to support pipeling, but has not seen the pipeline API directly. What’s going on?

3.4.1 track implementation pipeline

Pipeline is implemented using AsyncCommands and FlushCommands. After reading the official document, it can be known that synchronous and asynchronous commands of Lettuce actually share the same connection instance, and the bottom layer uses the form of pipeline to send/receive commands.

The difference is:

  • Sync command object obtained by the connection.sync() method, each operation immediately sends the command over the TCP connection;
  • Connection.async () gets an asynchronous command object that returns a RedisFuture
    , batch send only if certain conditions are met.

Thus we can implement the PIPELINE by means of asynchronous command + manual batch push. Let’s see 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 a problem with this?

When setAutoFlushCommands(false) is set, you will notice that the sync commands that the sync() method calls do not return! Why is that? Let’s look at the official documentation again:

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 their underlying implementations, except that sync simulates synchronization by blocking the calling thread. SetAutoFlushCommands is applied to the Connection object, so it applies to both the Sync and async command objects.

So, whenever a thread has auto flush commands set to false, 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); }}

Conversely, if multiple threads call async() to retrieve the set of asynchronous commands and then call flushCommands() after their own business logic is complete, it will force flush the asynchronous commands that are being appended by other threads, and the commands that were logically in a batch will be sent in multiple batches.

Although it does not affect the correctness of the results, if the threads interact with each other to shatter each other’s commands to send, the performance improvement will be very inconsistent.

It’s natural to think of creating a Connection for each batch command, and then… Doesn’t it rely on connection pooling just like Jedis?

Remembering Lao Wang’s piercing eyes behind his lenses, I decided to bite the bullet and dig again. Sure enough, after re-reading the documentation, I found another good thing: Batch Execution.

Rule 3.4.3 Batch Execution

Since FlushCommands has a global impact on the connection, why not limit flushing to the thread level? I found the sample official sample in the documentation.

Lettuce is a high-level client, it is really advanced after looking at the documentation, just need to define the interface (reminiscent of the Mapper interface of MyBatis), here 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); }

When called, do this:

/ / create the client RedisClusterClient client = RedisClusterClient. Create (DefaultClientResources. The create (), "redis: / /" + address); // The service contains an instance of the Factory. RedisCommandFactory Factory = new RedisCommandFactory(connect, Arrays.asList(ByteArrayCodec.INSTANCE, ByteArrayCodec.INSTANCE)); List<RedisFuture<? Class = List<RedisFuture<? >> futures = new ArrayList<>(); RedisBatchQuery batchQuery = factory.getCommands(RedisBatchQuery.class); for (RedisMetaGroup redisMetaGroup : AppendCommand (redisMetaGroup, futures, BatchQuery) {// Business logic that loops through multiple keys and saves the result in futures results; } // When the asynchronous command is called and the BATCHQUERY.FLUSH () is executed, the BATCHQUERY.FLUSH () command is sent to the Redis server;

It’s as simple as that.

Batch control is done at thread granularity and is performed when flush is called or when the number of caching commands configured at @batchSize is reached. For Connection instances, instead of setting Auto Flush Commands, the default is true, which does not affect other threads.

If a single command takes a long time to execute or someone puts a command such as BLPOP, it will have an impact. This topic is also covered in the official documentation. Consider using connection pooling to handle this problem.

3.5 Can you give it more power?

Well, Lettuce supports more than the simple functions mentioned above, but these are also worth a try:

3.5.1 Read/write separation

We know that Redis instances support master-slave deployment, synchronizing data asynchronously from the master instance, and using Redis Sentinel to switch between master and slave in the event of a master instance failure.

When applications are insensitive to data consistency and require high throughput, a master-slave read-write separation approach can be considered. Lettuce can set StatefulRedisClusterConnection readFrom configuration to adjust:

3.5.2 Configure automatic update of cluster topology

When using Redis Cluster, what happens to server capacity expansion?

The parameters can be configured by passing the clusterClientOptions object via the redisClusterClient# setOptions method (see the reference link at the end of this article for the full configuration).

Common configurations of topologyRefreshOptions in ClusterClientOptions are as follows:

3.5.3 connection pool

While the thread-safe, single-connection instance of Lettuce already has very good performance, it can’t be ruled out that some large businesses need thread pooling to improve throughput. Exclusive connections are also necessary for transactional operations.

Lettuce provides the ability to pool connections based on the Apache common-pool2 component:

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();

CreateCategory ObjectPool Set the wrapConnections parameter to true by default. In this case, the borrowed object’s close method will be overloaded as return connection via dynamic proxy. If set to false, the close method closes the connection.

Lettuce also supports asynchronous connection pooling (fetching a connection from a pool is an asynchronous operation). See link at the end of the article for details. There are many more features that can’t be enumerated, but can be found in the official documentation and examples that are well worth reading.

Fourth, the use of summary

Compared with Jedis, Lettuce is more convenient and abstract to use. The thread-safe connection reduces the number of connections in the system and improves the stability of the system.

For advanced players, there are also many configurations and interfaces that can be used to optimize performance and achieve in-depth business customization.

It must also be said that the official documentation of Lettuce is very comprehensive and detailed, which is very rare. Communities are active and commiters will actively answer all kinds of issues, which makes many questions can be solved by themselves.

By contrast, Jedis’s documentation, maintenance and updates are relatively slow. PR for Jediscluster Pipeline has not been integrated for four years (February 2021).

The resources

Two of the GitHub issues are very valuable and highly recommended to read!

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

2.Redis Java Clients

3. Website: https://lettuce.io

4.SpringDataRedis Reference Documentation

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