绝地武士与生菜:探索
作者: Guy Royse
我是一个真正的探索者,所以当我必须做出技术决定时——比如选择一个 Redis 客户端——我会去探险。这是我对 Java 客户端押韵组合的探索: Jedis 和 Lettuce。
我的计划很简单:
- 在代码中尝试一些简单的事情
- 在代码中尝试一些高级的东西
- 达到某种选择标准
- …
- 利润!
利润的格言目标,就像内裤一样,始终存在。 但您可以从中受益的部分是选择标准。这将使我们能够决定何时 Jedis 是正确的选择,而 Lettuce 何时才是正确的选择。这一点非常重要,因为我们都知道 选择工具时任何问题的答案都是“视情况而定”。
一种简单的代码
让我们比较一下所有练习中最简单的一些代码:从 Redis 的单个实例中设置和获取值。
首先,我们使用 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"
}
}
查看托管的 原始JedisSetGet.java
通过 GitHub
看代码,这很简单。创建连接。用它。关闭它。
接下来我们将使用生菜来做:
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 connection = redisClient.connect();
RedisCommands sync = connection.sync();
sync.set("foo", "bar");
String result = sync.get("foo");
connection.close();
redisClient.shutdown();
System.out.println(result); // "bar"
}
}
通过 GitHub 查看托管的 原始LettuceSetGet.java
这看起来有点复杂。有一个客户端、一个连接和一个命令对象。它们的名称和模板性质表明它们可能有多种变体。也许除了 StatefulRedisConnection
类型之外,我们还有一个接受 byte[]
的无状态变体?(剧透: 集群和主/副本配置有多种连接类型,但不是无状态的)。
但是,一旦您完成了设置和拆卸,这两个客户端中的基本代码都是相同的:创建连接。用它。关闭它。
现在,就这么简单的事情,Jedis 看起来更轻松。这是有道理的,因为它的代码更少。但我确信生菜拥有所有这些东西是有原因的——可能是为了处理更高级的场景。
管道、同步和异步
Jedis 都是同步的,除了管道。管道允许异步使用 Redis,但不幸的是,它不能与集群一起使用。但是,管道很容易使用:
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 get = p.get("foo");
p.zadd("baz", 13, "alpha");
p.zadd("baz", 23, "bravo");
p.zadd("baz", 42, "charlie");
Response> 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]
}
}
通过 GitHub 查看托管的 原始 JedisPipelining.java
如果你喜欢这种东西(我就是这样),Lettuce 支持同步、异步甚至反应式接口。然而,这些都只是 Lettuce 的多线程、基于事件的模型之上的语法糖层,理所当然地使用流水线。即使你同步使用它,它在下面也是异步的。
我们已经通过我们超级复杂的 set and get 示例看到了同步接口的实际作用。但是让我们看一下异步的:
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 connection = redisClient.connect();
RedisAsyncCommands 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"
}
}
通过 GitHub 查看托管的 原始LettuceAsync.java
这设置和获取,就像同步示例一样,但显然这是更多涉及的代码。它也是多线程的。
Jedis 和多线程代码
Jedis 可以很好地处理多线程应用程序,但是 Jedis 连接不是线程安全的。所以不要在线程间共享它们。如果你跨线程共享 Jedis 连接,Redis 会脱口而出各种协议错误,例如:
expected '$' but got ' '
为了解决这类问题,请使用 JedisPool——一个线程安全的对象,它分配线程不安全的 Jedis 对象。使用它很简单,就像其他绝地武士一样。完成后,只需请求一个线程并通过 .close()
将其返回到池中。这是在行动:
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 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"
}
}
通过 GitHub 查看托管的 原始JedisMultithreaded.java
这些 Jedis 对象中的每一个都封装了到 Redis 的单个连接,因此根据池的大小,可能存在阻塞或空闲连接。此外,这些连接是同步的,因此总是存在一定程度的空闲。
绝地武士、生菜和集群
我觉得我应该谈谈集群,但没什么可说的——至少在比较方面。有很多功能可以讨论,但两个库都支持它。不出所料,Jedis 更易于使用,但只能同步使用集群。Lettuce 更难使用,但能够与集群进行同步、异步和反应式交互。
这是反复出现的主题。这应该不足为奇。它自己承认 “Jedis 被认为易于使用”。Lettuce 在其主页上声明“Lettuce 是一个可扩展的 Redis 客户端,用于构建非阻塞反应式应用程序” 。
当然,如果您使用的是 Redis Enterprise,则不必担心集群,因为它是在服务器端处理的。只需使用 Jedis 或 Lettuce 的非集群 API,管理您的密钥,以便将它们分配到正确的分片中,您就可以开始了。
做出决定
那么,绝地武士还是生菜?这得看情况。(看,我告诉过你我们会在这里结束!)这是代码复杂性和应用程序可扩展性之间的经典权衡。
如果您需要高度可扩展的东西,请使用生菜。其更复杂的抽象提供了更轻松地制作可扩展产品的能力。Lettuce 是一个强大的解决方案,可让您使用 Redis 的全套功能。
如果您需要快速构建一些东西并且可扩展性不是并且可能不会成为问题,请使用 Jedis。它简单易用,让您更容易专注于应用程序和数据,而不是数据存储机制。
如果您仍然无法决定,您可以随时使用 Spring Data Redis,它将抽象出 Jedis 和 Lettuce,以便您将来改变主意。当然,这伴随着它自己的一系列权衡。但这是未来博客文章的主题!
由 RedisLabs 赞助