绝地武士与生菜:摸索

作者: 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<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"    }}

通过 GitHub 查看托管的 原始LettuceSetGet.java

这看起来有点简单。有一个客户端、一个连贯和一个命令对象。它们的名称和模板性质表明它们可能有多种变体。兴许除了 StatefulRedisConnection<String, String> 类型之外,咱们还有一个承受 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<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]    }}

通过 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<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"    }}

通过 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<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"    }}

通过 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 资助