Jedis vs. Lettuce: An Exploration
by Guy Royse
I’m an explorer by heart, so when I have to make a technical decision—like, say, choosing a Redis client—I go a-spelunking. Herein is the account of my exploration of the rhyming duo of Java clients: Jedis and Lettuce.
My plan was simple:
- Try some simple things in code
- Try some advanced things in code
- Arrive at some sort of selection criteria
- …
- Profit!
The gnomic goal of profit, like underpants, is always there. But the part that you can benefit from is the selection criteria. It will allow us to decide when Jedis is the right call and when Lettuce is the way to go. This is super important because we all know the answer to any question when selecting tools is, “it depends.”
A simple kind of code
Let’s compare some code for the simplest of all exercises: setting and getting a value from a single instance of Redis.
First, we do this with Jedis:
package com.guyroyse.blogs.lettucevsjedis; | |
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” | |
} | |
} |
view rawJedisSetGet.java hosted with
by GitHub
Looking at the code, this is pretty simple. Create a connection. Use it. Close it.
Next we’ll do it with Lettuce:
package com.guyroyse.blogs.lettucevsjedis; | |
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 rawLettuceSetGet.java hosted with
by GitHub
This looks a bit more involved. There’s a client, a connection, and a command object. And their names and templated nature suggest that there might be multiple varieties of them. Maybe in addition to a StatefulRedisConnection<String, String> type, we have a stateless variety that takes a byte[]? (Spoiler: there are multiple connection types for clustering and master/replica configurations, but not stateless ones).
Once you’re beyond the setup and teardown, however, it’s the same basic code in either client: Create a connection. Use it. Close it.
Right now, for something as simple as this, Jedis looks easier. Which makes sense, since it has less code. But I am certain that Lettuce has all this stuff for a reason—probably to handle more advanced scenarios.
Pipelines, sync, and async
Jedis is all synchronous with the exception of pipelines. Pipelines allow asynchronous usage of Redis but, unfortunately, cannot be used alongside clustering. However, pipelines are easy enough to use:
package com.guyroyse.blogs.lettucevsjedis; | |
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 rawJedisPipelining.java hosted with
by GitHub
Lettuce supports synchronous, asynchronous, and even reactive interfaces if you’re into that sort of thing (which I am). However, these are all just layers of syntactic sugar on top of Lettuce’s multi-threaded, event-based model that uses pipelining as a matter of course. Even when you use it synchronously, it’s asynchronous underneath.
We’ve seen the synchronous interface in action already with our super sophisticated set and get example. But let’s take a look at the asynchronous one:
package com.guyroyse.blogs.lettucevsjedis; | |
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 rawLettuceAsync.java hosted with
by GitHub
This sets and gets, just like the synchronous example, but clearly this is more involved code. It’s also multi-threaded.
Jedis and multi-threaded code
Jedis can handle multi-threaded applications just fine, but a Jedis connection is not thread-safe. So don’t share them. If you share a Jedis connection across threads, Redis will blurt out all sorts of protocol errors like:
expected '$' but got ' '
To solve these sorts of problems, use JedisPool—a thread-safe object that dispenses thread-unsafe Jedis objects. Using it is simple, like the rest of Jedis. Just ask for a thread and return it to the pool via .close() when you’re done. Here it is in action:
package com.guyroyse.blogs.lettucevsjedis; | |
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 rawJedisMultithreaded.java hosted with
by GitHub
Each of these Jedis objects encapsulates a single connection to Redis, so depending on how large your pool is there may be blocking or idle connections. Furthermore, these connections are synchronous so there’s always a degree of idleness.
Jedis, Lettuce, and clustering
I feel like I should talk about clustering, but there’s not much to say—at least in terms of comparisons. There’s plenty of capability to discuss but both libraries support it. Unsurprisingly, Jedis is easier to use but can work with clusters only synchronously. Lettuce is more difficult to use but capable of synchronous, asynchronous, and reactive interaction with the cluster.
This is the recurring theme. And that shouldn’t be surprising. By its own admission “Jedis was conceived to be EASY to use”. And Lettuce states “Lettuce is a scalable Redis client for building non-blocking Reactive applications” right on its homepage.
Of course, if you’re using Redis Enterprise, you don’t have to worry about clustering as it’s handled server-side. Just use the non-clustered APIs of Jedis or Lettuce, manage your keys so they’re slotted to the correct shards, and you’re good to go.
Making the decision
So, Jedis or Lettuce? Well, it depends. (See, 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 sophisticated abstractions provide the capabilities to make scalable products more easily. Lettuce is a powerful solution that lets you use the full set of Redis’ capabilities.
If you need to build something fast and scalability isn’t and probably won’t be a concern, use Jedis. It’s simple, easy to use, and makes it easier for you to focus on your application and data rather than the mechanism of your data’s storage.
If you still can’t decide, you can always use Spring Data Redis, which will abstract away Jedis and Lettuce so you can change your mind in the future. Of course, that comes with its own set of tradeoffs. But that is a topic for a future blog post!
Sponsored by RedisLabs