*在Java中使用Jedis操作Redis

*1. 概述

本文是 Jedis 简介,Jedis 是 Java 中用于 Redis 的客户端库——Redis 是一种流行的内存数据结构存储,也可以持久化到磁盘。它由基于键值的数据结构驱动来持久化数据,可用作数据库、缓存、消息代理等。

首先,我们将解释 Jedis 在哪些情况下有用以及它是什么。

在后续章节中,我们将详细阐述各种数据结构,并解释事务、管道和发布/订阅功能。最后介绍连接池和 Redis 集群。

*2. 为什么选择 Jedis?

Redis 在其官方网站上列出了最知名的客户端库。Jedis 有多个替代方案,但目前只有两个更值得推荐:lettuceRedisson

这两个客户端确实有一些独特的功能,如线程安全、透明重连处理和异步 API,而 Jedis 缺乏这些功能。

然而,Jedis 体积小巧,而且速度明显快于其他两个。此外,它是 Spring Framework 开发者的首选客户端库,并且在三者中拥有最大的社区。

*3. Maven 依赖

让我们从声明 pom.xml 中唯一需要的依赖开始:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.8.1</version>
</dependency>

如果你正在寻找该库的最新版本,请查看此页面

*4. Redis 安装

你需要安装并启动最新版本的 Redis 之一。我们此刻正在运行最新的稳定版本(3.2.1),但任何 3.x 之后的版本应该都可以。

这里可以找到有关 Linux 和 Macintosh 上 Redis 的更多信息,它们的基本安装步骤非常相似。Windows 不受官方支持,但这个移植版本维护得很好。

之后,我们可以直接深入并从 Java 代码连接到它:

Jedis jedis = new Jedis();

默认构造函数可以正常工作,除非你在非默认端口或远程机器上启动了服务,在这种情况下,你可以通过将正确的值作为参数传递给构造函数来正确配置它。

*5. Redis 数据结构

大多数原生操作命令都受到支持,而且方便的是,它们通常共享相同的方法名。

*5.1. 字符串

字符串是最基本的 Redis 值类型,当你需要持久化简单的键值数据类型时非常有用:

jedis.set("events/city/rome", "32,15,223,828");
String cachedResponse = jedis.get("events/city/rome");

变量 cachedResponse 将保存值 32,15,223,828。结合稍后讨论的过期支持,它可以作为 Web 应用程序接收的 HTTP 请求和其他缓存需求的闪电般快速且易于使用的缓存层。

*5.2. 列表

Redis 列表只是字符串列表,按插入顺序排序,这使它成为实现消息队列等功能的理想工具:

jedis.lpush("queue#tasks", "firstTask");
jedis.lpush("queue#tasks", "secondTask");

String task = jedis.rpop("queue#tasks");

变量 task 将保存值 firstTask。请记住,你可以序列化任何对象并将其持久化为字符串,因此队列中的消息可以在需要时携带更复杂的数据。

*5.3. 集合

Redis 集合是一个无序的字符串集合,当你想要排除重复成员时非常方便:

jedis.sadd("nicknames", "nickname#1");
jedis.sadd("nicknames", "nickname#2");
jedis.sadd("nicknames", "nickname#1");

Set<String> nicknames = jedis.smembers("nicknames");
boolean exists = jedis.sismember("nicknames", "nickname#1");

Java Set nicknames 的大小将为 2,第二次添加 nickname#1 被忽略了。此外,变量 exists 的值为 true,方法 sismember 使你能够快速检查特定成员是否存在。

*5.4. 哈希

Redis 哈希是 字符串 字段和 字符串 值之间的映射:

jedis.hset("user#1", "name", "Peter");
jedis.hset("user#1", "job", "politician");

String name = jedis.hget("user#1", "name");

Map<String, String> fields = jedis.hgetAll("user#1");
String job = fields.get("job");

如你所见,当你想要单独访问对象的属性时,哈希是一种非常便捷的数据类型,因为你不需要检索整个对象。

*5.5. 有序集合

有序集合类似于集合,其中每个成员都有一个关联的排名,用于对它们进行排序:

Map<String, Double> scores = new HashMap<>();

scores.put("PlayerOne", 3000.0);
scores.put("PlayerTwo", 1500.0);
scores.put("PlayerThree", 8200.0);

scores.entrySet().forEach(playerScore -> {
    jedis.zadd(key, playerScore.getValue(), playerScore.getKey());
});

String player = jedis.zrevrange("ranking", 0, 1).iterator().next();
long rank = jedis.zrevrank("ranking", "PlayerOne");

变量 player 将保存值 PlayerThree,因为我们正在检索排名第一的玩家,而他的分数最高。变量 rank 的值为 1,因为 PlayerOne 在排名中位列第二,且排名是从零开始的。

*6. 事务

事务保证原子性和线程安全操作,这意味着在 Redis 事务期间,其他客户端的请求永远不会被并发处理:

String friendsPrefix = "friends#";
String userOneId = "4352523";
String userTwoId = "5552321";

Transaction t = jedis.multi();
t.sadd(friendsPrefix + userOneId, userTwoId);
t.sadd(friendsPrefix + userTwoId, userOneId);
t.exec();

你甚至可以通过在实例化 Transaction 之前 "监视" 特定键来使事务的成功依赖于该键:

jedis.watch("friends#deleted#" + userOneId);

如果该键的值在事务执行之前发生变化,事务将不会成功完成。

*7. 管道

当我们必须发送多个命令时,我们可以将它们打包在一个请求中,通过使用管道来节省连接开销,这本质上是一种网络优化。只要操作相互独立,我们就可以利用这种技术:

String userOneId = "4352523";
String userTwoId = "4849888";

Pipeline p = jedis.pipelined();
p.sadd("searched#" + userOneId, "paris");
p.zadd("ranking", 126, userOneId);
p.zadd("ranking", 325, userTwoId);
Response<Boolean> pipeExists = p.sismember("searched#" + userOneId, "paris");
Response<Set<String>> pipeRanking = p.zrange("ranking", 0, -1);
p.sync();

String exists = pipeExists.get();
Set<String> ranking = pipeRanking.get();

注意我们不会直接访问命令响应,而是获得一个 Response 实例,在管道同步后,我们可以从中请求底层响应。

*8. 发布/订阅

我们可以使用 Redis 消息代理功能在我们的系统不同组件之间发送消息。确保订阅者和发布者线程不共享同一个 Jedis 连接。

*8.1. 订阅者

订阅并监听发送到某个频道的消息:

Jedis jSubscriber = new Jedis();
jSubscriber.subscribe(new JedisPubSub() {
    @Override
    public void onMessage(String channel, String message) {
        // 处理消息
    }
}, "channel");

Subscribe 是一个阻塞方法,你需要显式地从 JedisPubSub 取消订阅。我们重写了 onMessage 方法,但还有更多有用的方法可以重写。

*8.2. 发布者

然后只需从发布者线程向同一频道发送消息:

Jedis jPublisher = new Jedis();
jPublisher.publish("channel", "test message");

*9. 连接池

重要的是要知道我们处理 Jedis 实例的方式很幼稚。在实际场景中,你不希望在多线程环境中使用单个实例,因为单个实例不是线程安全的。

幸运的是,我们可以轻松创建一个 Redis 连接池供我们在需要时复用,只要你在使用完毕后将资源归还到池中,这个池就是线程安全且可靠的。

让我们创建 JedisPool

final JedisPoolConfig poolConfig = buildPoolConfig();
JedisPool jedisPool = new JedisPool(poolConfig, "localhost");

private JedisPoolConfig buildPoolConfig() {
    final JedisPoolConfig poolConfig = new JedisPoolConfig();
    poolConfig.setMaxTotal(128);
    poolConfig.setMaxIdle(128);
    poolConfig.setMinIdle(16);
    poolConfig.setTestOnBorrow(true);
    poolConfig.setTestOnReturn(true);
    poolConfig.setTestWhileIdle(true);
    poolConfig.setMinEvictableIdleTimeMillis(Duration.ofSeconds(60).toMillis());
    poolConfig.setTimeBetweenEvictionRunsMillis(Duration.ofSeconds(30).toMillis());
    poolConfig.setNumTestsPerEvictionRun(3);
    poolConfig.setBlockWhenExhausted(true);
    return poolConfig;
}

由于池实例是线程安全的,你可以将其静态存储在某处,但应注意在应用程序关闭时销毁池以避免泄漏。

现在,我们可以在应用程序的任何需要的地方使用我们的池:

try (Jedis jedis = jedisPool.getResource()) {
    // 使用 jedis 资源执行操作
}

我们使用了 Java try-with-resources 语句来避免必须手动关闭 Jedis 资源,但如果你不能使用此语句,你也可以在 finally 子句中手动关闭资源。

确保在你的应用程序中使用像我们描述的这样的池,如果你不想面对讨厌的多线程问题。你显然可以调整池配置参数以适应你系统中的最佳设置。

*10. Redis 集群

此 Redis 实现提供了轻松的扩展性和高可用性,如果你不熟悉它,我们鼓励你阅读其官方规范。我们不会介绍 Redis 集群设置,因为这有点超出本文的范围,但在阅读完其文档后,你应该能够轻松做到这一点。

一旦我们准备就绪,就可以从我们的应用程序开始使用它:

try (JedisCluster jedisCluster = new JedisCluster(new HostAndPort("localhost", 6379))) {
    // 像使用普通 Jedis 资源一样使用 jedisCluster 资源
} catch (IOException e) {}

我们只需要提供其中一个主实例的主机和端口详细信息,它将自动发现集群中的其余实例。

这当然是一个非常强大的功能,但它不是银弹。使用 Redis 集群时,你不能执行事务也不能使用管道,这是许多应用程序依赖的两个重要功能,用于确保数据完整性。

事务被禁用是因为,在集群环境中,键将跨多个实例持久化。对于涉及在不同实例中执行命令的操作,无法保证操作原子性和线程安全。

一些高级键创建策略将确保你希望在同一实例中持久化的数据会以这种方式持久化。理论上,这应该使你能够使用 Redis 集群的底层 Jedis 实例之一成功执行事务。

不幸的是,目前你无法使用 Jedis 找到特定键保存在哪个 Redis 实例中(这实际上是 Redis 原生支持的),因此你不知道必须在哪个实例上执行事务操作。如果你对此感兴趣,可以在这里找到更多信息。

*11. 结论

Redis 的绝大多数功能在 Jedis 中都已经可用,而且其开发进展良好。

它使你能够以很少的麻烦将强大的内存存储引擎集成到你的应用程序中,只是不要忘记设置连接池以避免线程安全问题。

你可以在 GitHub 项目 中找到代码示例。

*入门指南

*安装 Jedis

为了在你的应用程序中将 Jedis 作为依赖项,你可以:

*使用 jar 文件

search.maven.org 或任何其他 maven 仓库下载最新的 JedisApache Commons Pool2 jar 文件。

*从源码构建

这为你提供最新的版本。

*克隆 GitHub 项目。

这非常简单,在命令行中你只需要执行:git clone git://github.com/xetorthio/jedis.git

*从 GitHub 构建

在使用 maven 打包之前,你必须通过测试。要运行测试并打包,请执行 make package

*配置 Maven 依赖

Jedis 也通过 Sonatype 作为 Maven 依赖项分发。要配置它,只需将以下 XML 片段添加到你的 pom.xml 文件中。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

*基本使用示例

*在多线程环境中使用 Jedis

你不应该从不同线程使用同一个实例,因为你会遇到奇怪的错误。有时创建大量 Jedis 实例也不够好,因为这意味着大量的套接字和连接,这也会导致奇怪的错误。单个 Jedis 实例不是线程安全的! 为了避免这些问题,你应该使用 JedisPool,它是一个线程安全的网络连接池。你可以使用该池可靠地创建多个 Jedis 实例,前提是你在使用完毕后将 Jedis 实例归还到池中。这样你可以克服那些奇怪的错误并实现出色的性能。

要使用它,初始化一个池:

JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost");

你可以将池静态存储在某处,它是线程安全的。

JedisPoolConfig 包含许多有用的 Redis 特定连接池默认值。JedisPool 基于 Commons Pool 2,因此你可能想查看 Commons Pool 的配置。更多详情请参见 http://commons.apache.org/proper/commons-pool/apidocs/org/apache/commons/pool2/impl/GenericObjectPoolConfig.html。

你通过以下方式使用它:

/// Jedis 实现了 Closeable。因此,jedis 实例将在最后一条语句后自动关闭。
try (Jedis jedis = pool.getResource()) {
  /// ... 在这里执行操作 ... 例如
  jedis.set("foo", "bar");
  String foobar = jedis.get("foo");
  jedis.zadd("sose", 0, "car"); jedis.zadd("sose", 0, "bike"); 
  Set<String> sose = jedis.zrange("sose", 0, -1);
}
/// ... 关闭应用程序时:
pool.close();

如果你不能使用 try-with-resource,你仍然可以享受 Jedis.close() 的便利。

Jedis jedis = null;
try {
  jedis = pool.getResource();
  /// ... 在这里执行操作 ... 例如
  jedis.set("foo", "bar");
  String foobar = jedis.get("foo");
  jedis.zadd("sose", 0, "car"); jedis.zadd("sose", 0, "bike"); 
  Set<String> sose = jedis.zrange("sose", 0, -1);
} finally {
  // 你必须关闭 jedis 对象。如果不关闭,
  // 它不会释放回池中,你也就无法从池中获取新资源。
  if (jedis != null) {
    jedis.close();
  }
}
/// ... 关闭应用程序时:
pool.close();

如果 Jedis 是从池中借用的,它将使用适当的方法归还到池中,因为它已经确定发生了 JedisConnectionException。如果 Jedis 不是从池中借用的,它将被断开并关闭。

*设置主从分布

*启用复制

Redis 主要是为主从分布构建的。这意味着写入请求必须显式地发送到主节点(一个 redis 服务器),主节点将更改复制到从节点(也是 redis 服务器)。然后读取请求可以(但不一定必须)发送到从节点,这减轻了主节点的压力。

你如上所示使用主节点。要启用复制,有两种方法告诉从节点它将成为给定主节点的 "slaveOf":

  • 在 redis 服务器的 Redis 配置文件中的相应部分指定它
  • 在给定的 jedis 实例上(见上文),调用 slaveOf 方法并传递 IP(或 "localhost")和端口作为参数:
jedis.slaveof("localhost", 6379);  // 如果主节点与运行代码的同一台 PC 上
jedis.slaveof("192.168.1.35", 6379); 

注意:从 Redis 2.6 开始,从节点默认是只读的,因此对它们的写入请求将导致错误。

如果你更改该设置,它们将像普通的 redis 服务器一样运行并接受写入请求而不报错,但更改不会被复制,因此这些更改有被静默覆盖的风险,如果你混淆了你的 jedis 实例。

*禁用复制 / 主节点故障时提升从节点

如果你的主节点宕机,你可能希望提升一个从节点成为新的主节点。你应该首先(尝试)禁用离线主节点的复制,然后,如果你有多个从节点,启用剩余从节点到新主节点的复制:

slave1jedis.slaveofNoOne();
slave2jedis.slaveof("192.168.1.36", 6379);