1. 概述

本文将介绍 Jedis——一个专为 Redis 设计的 Java 客户端库。Redis 作为流行的内存数据结构存储系统,既能完全在内存中运行,也能持久化到磁盘。它基于键值对存储数据,可作为数据库、缓存或消息代理等使用。

我们将从 Jedis 的核心概念入手,探讨其适用场景。随后深入讲解 Redis 的各种数据结构操作,并介绍事务、管道(Pipelining)以及发布/订阅功能。最后讨论连接池和 Redis 集群的使用要点。

2. 为什么选择 Jedis?

Redis 官方网站列出了主流客户端库,其中 Jedis 有两个强劲对手:lettuce 和 Redisson。这两个库确实具备 Jedis 缺乏的特性:

  • 线程安全
  • 透明重连处理
  • 异步 API

但 Jedis 也有显著优势:

  • 轻量高效:体积小,性能明显优于前两者
  • 生态完善:Spring 框架首选 Redis 客户端,社区最活跃
  • 简单粗暴:API 直观,学习成本低

踩坑提示:在纯性能敏感场景,Jedis 往往是最佳选择;但若需复杂功能(如异步操作),可考虑其他库。

3. Maven 依赖

pom.xml 中添加依赖即可开始使用:

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

最新版本可在 Maven Central 查询。

4. Redis 安装

安装并启动 Redis(推荐 3.x 以上版本)。Linux/Mac 用户参考官方文档,Windows 用户可使用维护良好的第三方版本。

连接代码极其简单:

Jedis jedis = new Jedis();

默认构造函数适用于本地标准端口部署。如需自定义配置:

Jedis jedis = new Jedis("redis.example.com", 6379);

5. Redis 数据结构操作

Jedis 几乎支持所有原生命令,且方法名与 Redis 命令高度一致。

5.1 字符串(Strings)

最基础的数据类型,适用于简单键值存储:

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

cachedResponse 将获取值 "32,15,223,828"。配合过期机制(后文详述),可作为高效的 HTTP 请求缓存层。

5.2 列表(Lists)

按插入顺序排序的字符串集合,天然适合实现消息队列:

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

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

task 将获取 "firstTask"(先进先出)。可通过序列化存储复杂对象。

5.3 集合(Sets)

无序且元素唯一的字符串集合:

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

nicknames 大小为 2(重复元素被忽略),existstruesismember 方法提供 O(1) 复杂度的成员检测。

5.4 哈希(Hashes)

字符串字段到字符串值的映射,适合存储对象:

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 有序集合(Sorted Sets)

带排序分值的集合,适用于排行榜场景:

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(排名从 0 开始)。

6. 事务(Transactions)

事务保证操作的原子性与线程安全:

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

可通过 watch 实现乐观锁:

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

若被监视键在事务执行前被修改,事务将失败。

7. 管道(Pipelining)

打包多个命令减少网络开销,适用于批量操作:

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 对象获取结果,需在 sync() 后调用 get()

8. 发布/订阅(Publish/Subscribe)

实现组件间消息传递,注意订阅者和发布者必须使用独立连接

8.1 订阅者

监听频道消息:

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

subscribe 是阻塞方法,需显式取消订阅。可重写更多回调方法(如 onSubscribeonUnsubscribe)。

8.2 发布者

向频道发送消息:

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

9. 连接池(Connection Pooling)

重要:直接使用 Jedis 实例在多线程环境不安全!必须使用连接池。

创建线程安全的连接池:

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()) {
    // 执行操作
}

踩坑提醒:务必用 try-with-resources 或 finally 块确保资源归还!

10. Redis 集群(Redis Cluster)

提供高可用与水平扩展能力。配置参考官方文档

集群操作示例:

try (JedisCluster jedisCluster = new JedisCluster(new HostAndPort("localhost", 6379))) {
    // 使用方式与普通 Jedis 一致
} catch (IOException e) {}

只需提供任一主节点地址,即可自动发现集群节点。

限制

  • 不支持事务(因键可能分布在不同节点)
  • 不支持管道操作
  • 需通过哈希标签(如 {user1}:profile)确保相关键在同一节点

深坑:Jedis 无法直接查询键所在节点,导致跨节点事务难以实现(Redis 原生支持此功能,但 Jedis 未暴露)。

11. 总结

Jedis 覆盖了 Redis 绝大多数功能,且持续迭代更新。它能以极低成本为应用注入强大的内存存储能力。

核心建议

  • 生产环境必须使用连接池
  • 简单场景优先选 Jedis,复杂需求可考虑其他客户端
  • 集群环境下需重新设计数据模型

本文源码见 GitHub 项目


原始标题:Intro to Jedis - the Java Redis Client Library