Jedis – Java Redis 客户端库的介绍

评论 0 浏览 0 2016-07-13

1.概述

在本教程中,我们将介绍Jedis,这是一个用于Redis的Java客户端库。这个流行的内存数据结构存储也可以在磁盘上持久化。它由一个基于keystore的数据结构来驱动数据的持久化,并可作为数据库、缓存、消息代理等使用。

我们将首先讨论Jedis是什么,以及它在什么样的情况下是有用的。然后,我们将详细介绍各种数据结构,并解释事务、管道和发布/订阅功能。最后,我们将学习连接池和Redis集群。

2.为什么是Jedis?

Redis在他们的官方网站上列出了最知名的客户端库。Jedis有多个替代品,但目前只有两个值得他们推荐的明星,lettuce,和Redisson

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

然而,Jedis很小,而且比其他两个快得多。此外,它是Spring框架开发者的首选客户端库,而且它拥有所有三个库中最大的社区。

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.Set 集合

Redis Set是一个无序的字符串集合,当我们想排除重复的成员时,它就会派上用场:

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集合nicknames的大小将为2,因为nickname#1的第二次添加被忽略了。另外,exists变量的值将是true。方法sismember使我们能够快速检查某个成员的存在。

5.4.哈希值

Redis哈希值是String字段和String值之间的映射关系:

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是排名第二的,而且排名是以0为基础的。

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();

我们甚至可以在实例化我们的事务之前,通过“观察”它,使事务的成功取决于一个特定的键:

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) {
        // handle message
    }
}, "channel");

订阅是一个阻塞的方法;我们需要明确地取消订阅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()) {
    // do operations with jedis resource
}

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

如果我们不想面对讨厌的多线程问题,在我们的应用程序中使用一个像我们所描述的池是很重要的。我们还可以玩玩池的配置参数,使其适应我们系统的最佳设置。

10.Redis群集

这个Redis实现提供了轻松的可扩展性和高可用性。为了更熟悉它,我们可以查看他们的官方规范。我们不会涉及Redis集群的设置,因为这有点超出了本文的范围,但我们在完成文档后应该不会有任何问题。

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

try (JedisCluster jedisCluster = new JedisCluster(new HostAndPort("localhost", 6379))) {
    // use the jedisCluster resource as if it was a normal Jedis resource
} catch (IOException e) {}

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

这当然是一个非常强大的功能,但它不是一个灵丹妙药。当使用Redis Cluster时,我们不能执行事务或使用管道,这是许多应用程序为确保数据完整性而依赖的两个重要功能。

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

一些高级的密钥创建策略将确保我们想要在同一实例中持久化的数据将得到这样的持久化。理论上,这应该能让我们使用Redis集群的一个底层Jedis实例成功执行事务。

不幸的是,我们目前无法使用Jedis(实际上Redis原生支持)找出某个特定的键保存在哪个Redis实例中,所以我们不知道我们必须对哪个实例进行事务操作。如果我们想了解更多信息,可以这里

11.结语

Redis的绝大部分功能在Jedis中已经可以使用了,而且它的开发速度也很好地向前推进。

它使我们有能力在我们的应用程序中集成一个强大的内存存储引擎,而不需要太多的麻烦。我们只是不能忘记设置连接池以避免线程安全问题。

像往常一样,本文的源代码可以在GitHub项目中找到。

最后更新2023-05-21
0 个评论
标签