Spring Data Redis

1. Spring Data Redis 简介

Spring Data Redis 是 Spring Data 家族的一部分,它为 Spring 应用提供了对 Redis 存储的高级抽象。它屏蔽了底层连接库(如 Jedis 或 Lettuce)的复杂实现细节,使开发者能够通过统一的 API 与 Redis 进行交互。

核心价值

  • 连接管理:自动管理 Redis 连接的生命周期及连接池。
  • 操作封装:将 Redis 原始命令封装为面向对象的 API。
  • 异常转化 :将底层的 Redis 异常转换为 Spring 统一的 DataAccessException 体系。

2. RedisTemplate 介绍

RedisTemplate 是 Spring Data Redis 的核心类,它提供了执行 Redis 操作的模板方法。

数据结构操作映射

RedisTemplate 将 Redis 的基本数据结构划分为不同的操作接口:

接口方法 对应的 Redis 数据结构
opsForValue() String (字符串)
opsForHash() Hash (哈希)
opsForList() List (列表)
opsForSet() Set (集合)
opsForZSet() ZSet (有序集合)

3. Spring Boot 集成步骤

Spring Boot 3.4.2 环境下,集成步骤如下:

3.1 引入依赖 (pom.xml)

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

3.2 配置连接信息 (application.yaml)

yaml 复制代码
spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      database: 0
      jedis:
        pool:
          max-active: 8 # 最大连接数
          max-idle: 8   # 最大空闲连接

4. RedisTemplate 常见操作示例

4.1 String 字符串操作

java 复制代码
@Autowired
private RedisTemplate<String, Object> redisTemplate;

public void testString() {
    // 写入数据
    redisTemplate.opsForValue().set("user:name", "czl");
    // 读取数据
    String value = (String) redisTemplate.opsForValue().get("user:name");
    // 带过期时间的写入
    redisTemplate.opsForValue().set("code", "1234", Duration.ofMinutes(5));
}

4.2 Hash 哈希操作

java 复制代码
public void testHash() {
    String key = "user:info:1";
    // 存入单个字段
    redisTemplate.opsForHash().put(key, "name", "tiger");
    // 获取单个字段
    Object name = redisTemplate.opsForHash().get(key, "name");
    // 获取全部字段
    Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
}

4.3 List 列表操作

java 复制代码
public void testList() {
    String key = "queue:task";
    // 从左侧推入
    redisTemplate.opsForList().leftPush(key, "task1");
    // 从右侧弹出
    Object task = redisTemplate.opsForList().rightPop(key);
}

4.4 Set 集合操作

java 复制代码
public void testSet() {
    String key = "tags";
    // 添加元素
    redisTemplate.opsForSet().add(key, "java", "redis", "spring");
    // 判断是否存在
    Boolean isMember = redisTemplate.opsForSet().isMember(key, "java");
}

5. RedisTemplate 与 Jedis 实例关系

在 Spring Boot 应用中,RedisTemplate 与底层 Jedis 实例之间并非一比一的绑定关系,而是一种典型的"多对一"或"一对多"的解耦模型

5.1 实例数量对比

一对多关系

  • RedisTemplate
    在一个 Spring Context 中,你通常只需要配置一个 RedisTemplate<String, Object> 就能在全工程中通过 @Autowired 共享。它是无状态的,专门负责定义操作逻辑和序列化规则。不过也可以配置多个定制化RedisTemplate,关键是与Jedis实例是一对多关系。
  • Jedis 实例:是一个连接池
    底层通过 JedisPool 维护了多个长连接实例。具体数量由你在 application.yaml 中配置的 max-active 决定。

5.2 动态映射关系

当你调用 redisTemplate.opsForValue().set(...) 时,发生的数量调度如下:

  1. 借用阶段RedisTemplate 拦截到请求,从底层的 JedisPool 中申请一个 当前的空闲 Jedis 实例。
  2. 独占阶段 :在命令执行期间,这个特定的 Jedis 实例被该线程独占,其他线程无法使用它。
  3. 归还阶段 :操作完成后,RedisTemplate 自动释放连接,将该 Jedis 实例归还给池中。

5.3 结论

  • 逻辑层 :你只需要维护 1 个 RedisTemplate 即可处理所有的并发请求。
  • 物理层 :底层的 JedisPool 会根据并发量动态分配 N 个 Jedis 实例来支持这 1 个模板的工作。

6. RedisTemplate 序列化器深度解析

6.1 为什么要使用序列化器?

Redis 数据库本身是二进制安全 的,它只能存储字节序列(byte array)。而 Java 是面向对象的语言,我们操作的是 StringInteger 或自定义的 UserDO 对象。

序列化器(Serializer)的作用:就是充当 Java 对象与 Redis 字节流之间的"翻译官"。

  • 存入时:将 Java 对象转换成二进制字节。
  • 读取时:将二进制字节转换回 Java 对象。
示例:不配置序列化器的后果

假设你使用 Spring 默认的 RedisTemplate(未手动配置序列化器),执行以下操作:

java 复制代码
redisTemplate.opsForValue().set("name", "虎哥");

虽然代码运行成功,但当你通过命令行或 Redis Insight 查看时,你会发现:

  • Key 变成了:\xac\xed\x00\x05t\x00\x04name
  • Value 变成了:\xac\xed\x00\x05t\x00\x06\xe8\x99\x8e\xe5\x93\xa5

这是因为默认使用了 JdkSerializationRedisSerializer,它在数据前添加了 Java 序列化协议的魔数(\xac\xed)和类元数据。这导致数据在 Redis 中不可读,且其他语言(如 Python、Go)无法解析。

6.2 序列化器的具体用法

在 Spring Boot 3.4.2 环境下,我们通过自定义 RedisConfig 类来指定序列化规则。

1. 配置代码示例

通常建议 Key 使用 String 序列化Value 使用 JSON 序列化,以达到最佳的可读性和兼容性。

java 复制代码
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // 1. 指定 Key 的序列化器:StringRedisSerializer
        // 效果:存入 "name",Redis 中显示为字符串 "name"
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());

        // 2. 指定 Value 的序列化器:GenericJackson2JsonRedisSerializer
        // 效果:存入对象,Redis 中显示为标准 JSON 字符串
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        return template;
    }
}
2. 配置后的存储效果对比

在使用上述配置后,同样的 set("name", "虎哥") 操作,Redis 中的表现如下:

存储部分 默认 Jdk 序列化(乱码) 配置后(明文/JSON)
Key \xac\xed\x00\x05t\x00\x04name name
Value \xac\xed\x00\x05t\x00... "虎哥"
对象 Value 二进制乱码 {"id":1, "nickname":"虎哥"}

7. Jackson 序列化器的内存占用问题及优化方案

7.1 Jackson 序列化器的"多余"字节码问题

在使用 GenericJackson2JsonRedisSerializer 时,为了实现自动反序列化,Jackson 会在生成的 JSON 字符串中额外添加一个名为 @class 的字段。

示例:自动序列化的存储结果

假设存储一个简单的 UserDO 对象:

json 复制代码
{
  "@class": "cn.iocoder.boot.springdataredis.UserDO",
  "id": 1,
  "nickname": "虎哥"
}
  • 问题所在 :这个 @class 字段包含了类全路径名,在大型项目中,这个字符串往往比业务数据本身还要长。
  • 内存影响:如果你有数百万个 Key,这些重复的类路径字符串会额外占用大量的 Redis 内存空间,增加硬件成本和网络带宽压力。

7.2 解决方案:手动序列化(String + ObjectMapper)

为了追求极致的内存利用率,业界常用的方案是:全局只使用 StringRedisTemplate(或 String 序列化器),在代码逻辑中手动利用 ObjectMapper(或 JsonUtils)进行对象转换

1. 核心思路
  • 存储时 :调用 JsonUtils.toJsonString(obj) 得到纯净的 JSON,不带 @class 标记,存入 Redis。
  • 读取时 :取出 String,根据业务需要调用 JsonUtils.parseObject(json, UserDO.class) 还原对象。
2. 代码实现示例

基于你已有的 Spring Boot 3.4.2 环境 和 JsonUtils

java 复制代码
@Service
public class UserService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate; // 默认已有的 String 模板

    public void saveUserManual(UserDO user) {
        // 1. 手动序列化为纯净 JSON 字符串
        String jsonStr = JsonUtils.toJsonString(user);
        
        // 2. 存入 Redis
        stringRedisTemplate.opsForValue().set("user:" + user.getId(), jsonStr);
    }

    public UserDO getUserManual(Long id) {
        // 3. 获取纯净 JSON 字符串
        String jsonStr = stringRedisTemplate.opsForValue().get("user:" + id);
        
        // 4. 手动反序列化,通过 Class 参数明确目标类型
        return JsonUtils.parseObject(jsonStr, UserDO.class);
    }
}

相关推荐
知识即是力量ol2 小时前
在客户端直接上传文件到OSS
java·后端·客户端·阿里云oss·客户端直传
闻哥2 小时前
深入理解 Spring @Conditional 注解:原理与实战
java·jvm·后端·python·spring
煜磊2 小时前
MD5加盐值-注册与登录
java·开发语言
东东5162 小时前
校园求职招聘系统设计和实现 springboot +vue
java·vue.js·spring boot·求职招聘·毕设
Cult Of2 小时前
锁正确使用
java
long3162 小时前
K‘ 未排序数组中的最小/最大元素 |期望线性时间
java·算法·排序算法·springboot·sorting algorithm
xqqxqxxq2 小时前
洛谷算法1-1 模拟与高精度(NOIP经典真题解析)java(持续更新)
java·开发语言·算法
MengFly_2 小时前
Compose 脚手架 Scaffold 完全指南
android·java·数据库
PPPPickup2 小时前
application.yml或者yaml文件不显示绿色问题
java·数据库·spring