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(重复元素被忽略),exists
为 true
。sismember
方法提供 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
是阻塞方法,需显式取消订阅。可重写更多回调方法(如 onSubscribe
、onUnsubscribe
)。
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 项目。