1
头图

The Jedi and the Lettuce: Exploration

Author: Guy Royse

I'm a true explorer, so when I have to make a technical decision - like choosing a Redis client - I go exploring. Here's my exploration of Java client-side rhyming combinations: Jedis and Lettuce .

My plan is simple:

  • Try something simple in your code
  • Try something advanced in your code
  • meet certain selection criteria
  • profit!

The motto goal of profit, like the panties, is always there . But the part you can benefit from is the selection criteria. This will allow us to decide when Jedis is the right choice and Lettuce is the right choice. This is very important because we all know that the answer to any question when choosing a tool is "it depends".

a simple code

Let's compare some of the simplest code of all the exercises: setting and getting values from a single instance of Redis.

First, we do this with Jedis:

 import redis.clients.jedis.Jedis;

public class JedisSetGet {
    private static final String YOUR_CONNECTION_STRING = "redis://:foobared@yourserver:6379/0";

    public static void main(String[] args) {
        Jedis jedis = new Jedis(YOUR_CONNECTION_STRING);
        jedis.set("foo", "bar");
        String result = jedis.get("foo");
        jedis.close();
        System.out.println(result); // "bar"
    }
}

Check out the managed original JedisSetGet.java

via GitHub

Looking at the code, it's pretty simple. Create a connection. use it. close it.

Next we will use lettuce to make:

 import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
public class LettuceSetGet {
    private static final String YOUR_CONNECTION_STRING = "redis://:foobared@yourserver:6379/0";
    public static void main(String[] args) {
        RedisClient redisClient = RedisClient.create(YOUR_CONNECTION_STRING);
        StatefulRedisConnection<String, String> connection = redisClient.connect();
        RedisCommands<String, String> sync = connection.sync();
        sync.set("foo", "bar");
        String result = sync.get("foo");
        connection.close();
        redisClient.shutdown();
        System.out.println(result); // "bar"
    }
}

View the original LettuceSetGet.java hosted via GitHub

This seems a little complicated. There is a client, a connection, and a command object. Their names and template nature suggest that there may be many variants of them. Maybe we have a stateless variant that accepts byte[] in addition to the StatefulRedisConnection<String, String> type? (Spoiler: There are multiple connection types for cluster and master/replica configurations, but not stateless).

However, once you've done setup and teardown, the basic code is the same in both clients: create the connection. use it. close it.

Now, for something as simple as that, Jedis looks easier. This makes sense because it's less code. But I'm sure there's a reason lettuce has all of these things - probably to handle more advanced scenarios.

Pipelines, sync and async

Jedis is all synchronous, except for pipes. Pipelines allow asynchronous use of Redis, but unfortunately it doesn't work with clusters. However, pipes are easy to use:

 import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
import redis.clients.jedis.Tuple;
import java.util.Set;
import java.util.stream.Collectors;
public class JedisPipelining {
    private static final String YOUR_CONNECTION_STRING = "redis://:foobared@yourserver:6379/0";
    public static void main(String[] args) {
        Jedis jedis = new Jedis(YOUR_CONNECTION_STRING);
        Pipeline p = jedis.pipelined();
        p.set("foo", "bar");
        Response<String> get = p.get("foo");
        p.zadd("baz", 13, "alpha");
        p.zadd("baz", 23, "bravo");
        p.zadd("baz", 42, "charlie");
        Response<Set<Tuple>> range = p.zrangeWithScores("baz", 0, -1);
        p.sync();
        jedis.close();
        System.out.println(get.get()); // "bar"
        System.out.println(range.get().stream()
                .map(Object::toString)
                .collect(Collectors.joining(" "))); // [alpha,13.0] [bravo,23.0] [charlie,42.0]
    }
}

View the original JedisPipelining.java hosted via GitHub

If you like this kind of stuff (I am), Lettuce supports synchronous, asynchronous and even reactive interfaces. However, these are just syntactic sugar layers on top of Lettuce's multithreaded, event-based model, using pipelining for granted. Even if you use it synchronously, it's asynchronous underneath.

We've already seen synchronous interfaces in action with our super-complex set and get example. But let's look at the asynchronous ones:

 import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.async.RedisAsyncCommands;
public class LettuceAsync {
    private static final String YOUR_CONNECTION_STRING = "redis://:foobared@yourserver:6379/0";
    public static void main(String[] args) {
        RedisClient redisClient = RedisClient.create(YOUR_CONNECTION_STRING);
        StatefulRedisConnection<String, String> connection = redisClient.connect();
        RedisAsyncCommands<String, String> async = connection.async();
        final String[] result = new String[1];
        async.set("foo", "bar")
                .thenComposeAsync(ok -> async.get("foo"))
                .thenAccept(s -> result[0] = s)
                .toCompletableFuture()
                .join();
        connection.close();
        redisClient.shutdown();
        System.out.println(result[0]); // "bar"
    }
}

View the original LettuceAsync.java hosted via GitHub

This sets and gets, just like the synchronous example, but obviously this is more involved code. It is also multithreaded.

Jedis and multithreaded code

Jedis can handle multithreaded applications well, but Jedis connections are not thread-safe. So don't share them between threads. If you share a Jedis connection across threads, Redis will blurt out various protocol errors, such as:

 expected '$' but got ' '

To solve this type of problem, use a JedisPool - a thread-safe object that allocates thread-unsafe Jedis objects. Using it is simple, just like any other Jedi. Once done, just request a thread and return it to the pool via .close() . This is in action:

 import redis.clients.jedis.*;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class JedisMultithreaded {
    private static final String YOUR_CONNECTION_STRING = "redis://:foobared@yourserver:6379/0";
    public static void main(String[] args) {
        JedisPool pool = new JedisPool(YOUR_CONNECTION_STRING);
        List<String> allResults = IntStream.rangeClosed(1, 5)
                .parallel()
                .mapToObj(n -> {
                    Jedis jedis = pool.getResource();
                    jedis.set("foo" + n, "bar" + n);
                    String result = jedis.get("foo" + n);
                    jedis.close();
                    return result;
                })
                .collect(Collectors.toList());
        pool.close();
        System.out.println(allResults); // "bar1, bar2, bar3, bar4, bar5"
    }
}

View the original JedisMultithreaded.java hosted via GitHub

Each of these Jedis objects encapsulates a single connection to Redis, so depending on the size of the pool, there may be blocked or idle connections. Also, these connections are synchronous, so there is always some degree of idleness.

Jedi Knights, Lettuce and Clusters

I feel like I should talk about clusters, but nothing to say - at least in terms of comparisons. There are a lot of features to discuss, but both libraries support it. Unsurprisingly, Jedis is easier to use, but only works with clusters synchronously. Lettuce is harder to use, but enables synchronous, asynchronous and reactive interaction with the cluster.

This is a recurring theme. This should come as no surprise. By its own admission "Jedis is considered easy to use". Lettuce states on its homepage that "Lettuce is an extensible Redis client for building non-blocking reactive applications".

Of course, if you're using Redis Enterprise, you don't have to worry about clustering as it's handled on the server side. Just use Jedis or Lettuce's non-clustered APIs, manage your keys so they get assigned to the correct shards, and you're good to go.

Make a decision

So, Jedi or lettuce? It depends on the situation. (Look, I told you we'd end up here!) This is a classic trade-off between code complexity and application scalability.

If you need something highly scalable, use lettuce. Its more complex abstraction provides the ability to make scalable products more easily. Lettuce is a powerful solution that lets you use the full set of features of Redis.

If you need to build something quickly and scalability is not and probably won't be an issue, use Jedis. It's simple and easy to use, making it easier to focus on the application and the data rather than the data storage mechanism.

If you still can't decide, you can always use Spring Data Redis , which will abstract out Jedis and Lettuce so you can change your mind in the future. Of course, this comes with its own set of tradeoffs. But that's the subject of a future blog post!

Sponsored by RedisLabs


Yujiaao
12.7k 声望4.7k 粉丝

[链接]