Java 是如何操作Redis的?从 Spring Data Redis中RedisTemplate 源码分析 ZSet 调用链

刚开始学习 Redis 时,我一直有一个疑问:

MySQL 可以通过 JDBC 操作,那么 Java 又是怎样把 ZADDZRANGE 等命令发送给 Redis 的?

在 Spring Data Redis 中,我们通常通过 RedisTemplate 操作 Redis:

java 复制代码
  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] 按照分数从小到大排列

看起来只有几行代码,背后却经历了完整的调用过程:

text 复制代码
RedisTemplate.opsForZSet()
→ DefaultZSetOperations
→ 序列化 key 和 value
→ RedisTemplate.execute()
→ RedisConnection
→ Lettuce 或 Jedis
→ Redis Server
→ 反序列化结果

接下来我们以 opsForZSet()range() 为例,分析 Spring Data Redis 操作 Redis 的核心原理。


一、opsForZSet() 返回的是什么?

RedisTemplate 本身没有把所有 ZSet 方法都直接暴露出来,而是提供了一个专门操作 ZSet 的接口:

java 复制代码
ZSetOperations<K, V>

它的默认实现类是:

java 复制代码
DefaultZSetOperations<K, V>

RedisTemplate 中的源码结构可以简化为:

java 复制代码
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 对象。

从源码可以得到两个结论:

  1. opsForZSet() 返回的实际对象是 DefaultZSetOperations
  2. zSetOps 是成员变量,并不是每次调用都重新创建。

二、为什么 Operations 中保存的是同一个 RedisTemplate?

java 复制代码
new DefaultZSetOperations<>(this)

这段代码位于 RedisTemplate 类中,因此这里的 this 指向当前的 RedisTemplate 对象。

DefaultZSetOperations 会把这个对象继续传给父类 AbstractOperations

java 复制代码
class AbstractOperations<K, V> {

    final RedisTemplate<K, V> template;

    AbstractOperations(RedisTemplate<K, V> template) {
        this.template = template;
    }
}

最终对象关系可以理解为:

text 复制代码
RedisTemplate@10175
└── zSetOps → DefaultZSetOperations@10223
                └── template → RedisTemplate@10175

内存中只有一个 RedisTemplate 对象,只是多个变量保存了它的引用。

但需要注意,DefaultZSetOperations 本身并不是 RedisTemplate,它只是持有了模板对象的引用。


三、ZADD 是如何执行的?

业务代码:

java 复制代码
zSetOps.add("zset1", "a", 10);

最终会进入 DefaultZSetOperations.add(),核心逻辑可以简化为:

java 复制代码
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));
}

这个方法完成了三件事:

text 复制代码
key   → 序列化为 byte[]
value → 序列化为 byte[]
score → 直接作为 double 传递

然后通过:

java 复制代码
connection.zAdd(rawKey, score, rawValue)

执行 Redis 的:

redis 复制代码
ZADD zset1 10 a

需要注意,Java 泛型并不决定 Redis 中真正保存的数据格式:

java 复制代码
RedisTemplate<String, String>

这里只表示 Java 编译阶段 key 和 value 应该是 String

真正决定数据如何转换成字节的是:

text 复制代码
keySerializer
valueSerializer

四、range() 的完整调用链

业务代码:

java 复制代码
Set<String> values =
        zSetOps.range("zset:demo", 0, -1);

其中:

text 复制代码
0  表示第一个成员
-1 表示最后一个成员

因此:

java 复制代码
range(key, 0, -1)

表示按照排名查询全部成员,而不是按照分数范围查询。

它的整体调用链为:

text 复制代码
DefaultZSetOperations.range()
→ rawKey()
→ RedisTemplate.execute()
→ RedisConnection.zRange()
→ 返回 Set<byte[]>
→ deserializeValues()
→ 返回 Set<V>

如图: 我把range方法的调用过程截图展示出来

range() 先序列化 key,再通过 RedisTemplate 获取连接并执行 zRange,最后把 Redis 返回的字节数据反序列化为 Java 对象。


五、range() 源码做了什么?

核心源码可以简化为:

java 复制代码
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、实体类等对象。

因此:

java 复制代码
String key = "zset:demo";

需要先转换为:

java 复制代码
byte[]

rawKey() 的核心逻辑为:

java 复制代码
byte[] rawKey(Object key) {
    Assert.notNull(key, "non null key required");

    return keySerializer.serialize(key);
}

如果没有配置序列化器,并且 key 本身已经是 byte[],则可以直接使用;否则会调用 key serializer 完成序列化。


2. execute() 获取连接并执行命令

DefaultZSetOperations 自己不负责创建 Redis 连接。

它会通过父类中保存的 template 调用:

java 复制代码
template.execute(callback);

RedisTemplate.execute() 主要负责:

text 复制代码
通过 RedisConnectionFactory 获取连接
→ 执行回调中的 Redis 命令
→ 处理异常
→ 释放连接

回调代码是:

java 复制代码
connection ->
        connection.zRange(rawKey, start, end)

可以理解为:

当 RedisTemplate 给我一个 RedisConnection 后,就调用它的 zRange() 方法。

通过连接工厂获取连接。

是否使用连接池,取决于项目使用的是 Lettuce 还是 Jedis,以及具体的连接配置。


3. deserializeValues() 反序列化结果

Redis 返回的成员在底层表现为:

java 复制代码
Set<byte[]>

例如:

text 复制代码
byte[]("a")
byte[]("b")

Spring Data Redis 还需要调用:

java 复制代码
deserializeValues(rawValues)

使用 value serializer 把每个字节数组恢复为 Java 对象:

text 复制代码
Set<byte[]>
→ 逐个反序列化
→ Set<String>

最终业务代码拿到的才是:

java 复制代码
Set<String>

六、RedisConnection 之后发生了什么?

RedisConnection 是 Spring Data Redis 定义的统一连接接口。

如果项目使用 Lettuce,大致调用过程为:

text 复制代码
RedisConnection.zRange()
→ LettuceZSetCommands.zRange()
→ Lettuce客户端
→ 编码Redis命令
→ 通过网络发送给Redis Server

如果使用 Jedis,则会进入对应的 Jedis 实现。

这样设计的好处是,上层的 RedisTemplate 和业务代码不需要关心底层到底使用哪个 Redis 客户端。

完整流程可以表示为:

text 复制代码
Java代码
→ ZSetOperations
→ RedisTemplate
→ RedisConnection
→ Lettuce或Jedis
→ Redis Server

总结

通过 ZSetOperations.range() 的源码,可以得到以下结论:

  1. RedisTemplate 会提前创建并保存 DefaultZSetOperations 对象。
  2. opsForZSet() 只是返回已有对象,并不会每次重新创建。
  3. DefaultZSetOperations 持有原 RedisTemplate 的引用。
  4. Java 中的 key 和 value 需要先通过序列化器转换为 byte[]
  5. Operations 将具体操作交给 RedisTemplate.execute()
  6. RedisTemplate 负责获取连接、执行回调和释放连接。
  7. RedisConnection 最终通过 Lettuce 或 Jedis 将命令发送给 Redis。
  8. Redis 返回的字节数据还需要经过反序列化,才能变成 Java 对象。

整个 range() 调用可以概括为:

text 复制代码
Java key
→ 序列化为 byte[]
→ 执行 ZRANGE
→ 获得 Set<byte[]>
→ 反序列化
→ 返回 Set<V>

ZSetOperations 是面向 ZSet 的操作入口,而序列化、连接管理和命令执行等基础能力,仍然由 RedisTemplate 统一提供。

相关推荐
凌虚2 小时前
Kubernetes 编年史:从 Borg 到云原生操作系统
后端·程序员·kubernetes
SelectDB2 小时前
金城银行基于 Apache Doris 构建实时数据平台:T+1 到分钟级的金融级实践
后端
Csvn2 小时前
📊 SQL 入门 Day 13:窗口函数进阶
后端·sql
卷无止境2 小时前
拯救乱码方块:pandas 绘图中文字体的一揽子解决方案
后端·python
SelectDB3 小时前
Apache Doris 4.1:面向 AI & Search 的统一数据底座怎么选?向量检索 + 全文搜索 + 100MB JSON 完整能力拆解
后端
lichenyang4533 小时前
从图片生成任务到用户隔离:AIGC Creative Studio 后端与 PostgreSQL 建模实践
前端·后端
Zane19943 小时前
别再手写 try/finally 了:一文讲透 with 语句背后的上下文管理器协议
后端·python
进击的程序猿~3 小时前
Go 内存分配与垃圾回收源码深度学习手册
开发语言·后端·golang
geovindu3 小时前
go:Bit Operation Algorithm
开发语言·后端·算法·golang·位运算法