刚开始学习 Redis 时,我一直有一个疑问:
MySQL 可以通过 JDBC 操作,那么 Java 又是怎样把
ZADD、ZRANGE等命令发送给 Redis 的?
在 Spring Data Redis 中,我们通常通过 RedisTemplate 操作 Redis:
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
//zadd
zSetOperations.add("zset1","a",10);
zSetOperations.add("zset1","b",12);
zSetOperations.add("zset1","c",7);
//当在redis库中有对应的value值时,再添加score时会进行覆盖
//zrange
Set zset1 = zSetOperations.range("zset1", 0, -1);
System.out.println(zset1);
//[c, a, b] 按照分数从小到大排列
看起来只有几行代码,背后却经历了完整的调用过程:
RedisTemplate.opsForZSet()
→ DefaultZSetOperations
→ 序列化 key 和 value
→ RedisTemplate.execute()
→ RedisConnection
→ Lettuce 或 Jedis
→ Redis Server
→ 反序列化结果
接下来我们以 opsForZSet() 和 range() 为例,分析 Spring Data Redis 操作 Redis 的核心原理。
一、opsForZSet() 返回的是什么?
RedisTemplate 本身没有把所有 ZSet 方法都直接暴露出来,而是提供了一个专门操作 ZSet 的接口:
ZSetOperations<K, V>
它的默认实现类是:
DefaultZSetOperations<K, V>
RedisTemplate 中的源码结构可以简化为:
public class RedisTemplate<K, V> {
private final ZSetOperations<K, V> zSetOps =
new DefaultZSetOperations<>(this);
public ZSetOperations<K, V> opsForZSet() {
return this.zSetOps;
}
}
如图
RedisTemplate 创建并保存 DefaultZSetOperations, 调用 opsForZSet() 时直接返回已有的 zSetOps 对象。
从源码可以得到两个结论:
opsForZSet()返回的实际对象是DefaultZSetOperations;zSetOps是成员变量,并不是每次调用都重新创建。
二、为什么 Operations 中保存的是同一个 RedisTemplate?
new DefaultZSetOperations<>(this)
这段代码位于 RedisTemplate 类中,因此这里的 this 指向当前的 RedisTemplate 对象。
DefaultZSetOperations 会把这个对象继续传给父类 AbstractOperations:
class AbstractOperations<K, V> {
final RedisTemplate<K, V> template;
AbstractOperations(RedisTemplate<K, V> template) {
this.template = template;
}
}
最终对象关系可以理解为:
RedisTemplate@10175
└── zSetOps → DefaultZSetOperations@10223
└── template → RedisTemplate@10175
内存中只有一个 RedisTemplate 对象,只是多个变量保存了它的引用。
但需要注意,DefaultZSetOperations 本身并不是 RedisTemplate,它只是持有了模板对象的引用。
三、ZADD 是如何执行的?
业务代码:
zSetOps.add("zset1", "a", 10);
最终会进入 DefaultZSetOperations.add(),核心逻辑可以简化为:
public Boolean add(K key, V value, double score) {
byte[] rawKey = rawKey(key);
byte[] rawValue = rawValue(value);
return execute(connection ->
connection.zAdd(rawKey, score, rawValue));
}
这个方法完成了三件事:
key → 序列化为 byte[]
value → 序列化为 byte[]
score → 直接作为 double 传递
然后通过:
connection.zAdd(rawKey, score, rawValue)
执行 Redis 的:
ZADD zset1 10 a
需要注意,Java 泛型并不决定 Redis 中真正保存的数据格式:
RedisTemplate<String, String>
这里只表示 Java 编译阶段 key 和 value 应该是 String。
真正决定数据如何转换成字节的是:
keySerializer
valueSerializer
四、range() 的完整调用链
业务代码:
Set<String> values =
zSetOps.range("zset:demo", 0, -1);
其中:
0 表示第一个成员
-1 表示最后一个成员
因此:
range(key, 0, -1)
表示按照排名查询全部成员,而不是按照分数范围查询。
它的整体调用链为:
DefaultZSetOperations.range()
→ rawKey()
→ RedisTemplate.execute()
→ RedisConnection.zRange()
→ 返回 Set<byte[]>
→ deserializeValues()
→ 返回 Set<V>
如图: 我把range方法的调用过程截图展示出来
range() 先序列化 key,再通过 RedisTemplate 获取连接并执行 zRange,最后把 Redis 返回的字节数据反序列化为 Java 对象。
五、range() 源码做了什么?
核心源码可以简化为:
public Set<V> range(K key, long start, long end) {
byte[] rawKey = rawKey(key);
Set<byte[]> rawValues =
execute(connection ->
connection.zRange(rawKey, start, end));
return deserializeValues(rawValues);
}
整个方法分为三个阶段。
1. rawKey() 序列化 key
Redis 服务器不认识 Java 中的 String、实体类等对象。
因此:
String key = "zset:demo";
需要先转换为:
byte[]
rawKey() 的核心逻辑为:
byte[] rawKey(Object key) {
Assert.notNull(key, "non null key required");
return keySerializer.serialize(key);
}
如果没有配置序列化器,并且 key 本身已经是 byte[],则可以直接使用;否则会调用 key serializer 完成序列化。
2. execute() 获取连接并执行命令
DefaultZSetOperations 自己不负责创建 Redis 连接。
它会通过父类中保存的 template 调用:
template.execute(callback);
RedisTemplate.execute() 主要负责:
通过 RedisConnectionFactory 获取连接
→ 执行回调中的 Redis 命令
→ 处理异常
→ 释放连接
回调代码是:
connection ->
connection.zRange(rawKey, start, end)
可以理解为:
当 RedisTemplate 给我一个 RedisConnection 后,就调用它的
zRange()方法。
通过连接工厂获取连接。
是否使用连接池,取决于项目使用的是 Lettuce 还是 Jedis,以及具体的连接配置。
3. deserializeValues() 反序列化结果
Redis 返回的成员在底层表现为:
Set<byte[]>
例如:
byte[]("a")
byte[]("b")
Spring Data Redis 还需要调用:
deserializeValues(rawValues)
使用 value serializer 把每个字节数组恢复为 Java 对象:
Set<byte[]>
→ 逐个反序列化
→ Set<String>
最终业务代码拿到的才是:
Set<String>
六、RedisConnection 之后发生了什么?
RedisConnection 是 Spring Data Redis 定义的统一连接接口。
如果项目使用 Lettuce,大致调用过程为:
RedisConnection.zRange()
→ LettuceZSetCommands.zRange()
→ Lettuce客户端
→ 编码Redis命令
→ 通过网络发送给Redis Server
如果使用 Jedis,则会进入对应的 Jedis 实现。
这样设计的好处是,上层的 RedisTemplate 和业务代码不需要关心底层到底使用哪个 Redis 客户端。
完整流程可以表示为:
Java代码
→ ZSetOperations
→ RedisTemplate
→ RedisConnection
→ Lettuce或Jedis
→ Redis Server
总结
通过 ZSetOperations.range() 的源码,可以得到以下结论:
RedisTemplate会提前创建并保存DefaultZSetOperations对象。opsForZSet()只是返回已有对象,并不会每次重新创建。DefaultZSetOperations持有原RedisTemplate的引用。- Java 中的 key 和 value 需要先通过序列化器转换为
byte[]。 - Operations 将具体操作交给
RedisTemplate.execute()。 RedisTemplate负责获取连接、执行回调和释放连接。RedisConnection最终通过 Lettuce 或 Jedis 将命令发送给 Redis。- Redis 返回的字节数据还需要经过反序列化,才能变成 Java 对象。
整个 range() 调用可以概括为:
Java key
→ 序列化为 byte[]
→ 执行 ZRANGE
→ 获得 Set<byte[]>
→ 反序列化
→ 返回 Set<V>
ZSetOperations是面向 ZSet 的操作入口,而序列化、连接管理和命令执行等基础能力,仍然由RedisTemplate统一提供。