03-Spring Boot 整合 Redis:连接配置、序列化与工具类
📝 笔记摘要:Spring Boot 整合 Redis 的连接配置、序列化与工具类
Spring Boot 接 Redis 只要一个 starter,但默认配置有三个坑:配置前缀写错不报错、默认的 JDK 序列化让 key 变成乱码、Jackson 2 的序列化器在新版本里已经废弃。这篇按依赖、配置、序列化、模板、工具类五步走,每一步都给出 Spring Boot 4 上能跑的写法。
Redis 系列 共 4 篇,这是第 3 篇
01 Redis 核心 数据结构、持久化与内存治理
02 Redis 部署与高可用 容器化部署、主从复制、哨兵切换
03-Spring Boot 整合 Redis 连接配置、序列化、工具类 ← 本篇
04 Redis 缓存实战 穿透击穿雪崩、双写一致、分布式锁
这篇的结构
1. 依赖与客户端选型
2. 配置:前缀早就改过了
3. 序列化:默认的 JDK 序列化是个坑
4. 自定义 RedisTemplate
5. RedisUtil 工具类
6. 什么时候还需要 Jedis
1. 依赖与客户端选型
一个 starter 就够:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
这个 starter 默认带的是 Lettuce,不是 Jedis。在 Spring Initializr 的依赖选择器里,它对应的勾选项名字就叫「Spring Data Redis (Access+Driver)」——名字里的 Driver 就是它把客户端一起带进来了:

Lettuce 基于 Netty,一条 TCP 连接多路复用,线程安全,能被多线程共享。Spring Boot 2.0 起就是默认客户端
Jedis 每个线程一条连接,靠连接池复用。非线程安全,多线程用必须走池
Redisson 第三个实现,自带对象层和分布式锁,能满足同一套 SPI,但连接和池化自己管
提示:不要手工指定 Jackson 的版本号。Spring Boot 的依赖管理已经管着 Jackson 3 了,自己在 pom 里写死一个
jackson-databind的版本会覆盖 BOM 里的版本号,然后跟 Spring Data Redis 用的 Jackson 对不上,报出一堆莫名其妙的序列化异常。要控制版本就去改 Spring Boot 的版本,不要单独钉子。
确实需要连接池的时候(Lettuce 默认不需要),加 commons-pool2:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
项目怎么建、依赖怎么管、私服怎么配,见 Maven 入门与私有仓库搭建。
2. 配置:前缀早就改过了
这是最容易踩的一个坑,而且它不报错——配置写错了,Spring Boot 只是安静地忽略,你的应用连到默认的 127.0.0.1:6379 去了。
错的写法
spring.redis.host、spring.redis.port、spring.redis.password
对的写法spring.data.redis.host、spring.data.redis.port、spring.data.redis.password
前缀是在 Spring Boot 3.0 改的,不是 4.0。Spring Boot 4 没有再动 Redis 的键,但动了 Spring Session 的:spring.session.redis.* 变成了 spring.session.data.redis.*。Redisson 自己的 spring.redis.redisson.* 前缀一直没变。
spring:
data:
redis:
host: 127.0.0.1
port: 6379
database: 0
username: app
password: ${REDIS_PASSWORD}
# 读命令的超时,Boot 3.0 起支持带单位的时间写法
timeout: 2s
connect-timeout: 1s
client-name: order-service
# lettuce 或者 jedis,Boot 4.0 新增
client-type: lettuce
ssl:
enabled: false
lettuce:
pool:
enabled: true
max-active: 16
max-idle: 8
min-idle: 0
max-wait: -1
池化是 classpath 驱动的 只要
commons-pool2在 classpath 上,Boot 就会自动配置池化连接工厂。在 Lettuce 上这通常不是你想要的——多路复用连接本来就不需要池,加上池反而多一层
url 和 host/port 同时设置时 url 赢 两个都写,host、port、username、password全被忽略。两个都设是排查一整个下午的经典原因,二选一
密码别写死在 yml 里 用${REDIS_PASSWORD}从环境变量读,配置文件进 Git 不会泄露
timeout 别留空 Lettuce 的命令超时默认很宽松,请求队列又无界。Redis 挂掉的时候,故障会表现成堆内存持续上涨而不是快速失败,等 OOM 了才知道是超时没设
哨兵模式同样在 spring.data.redis 下面:
spring:
data:
redis:
sentinel:
master: mymaster
nodes:
- 172.17.0.2:26379
- 172.17.0.3:26379
- 172.17.0.4:26379
password: ${REDIS_PASSWORD}
配好这几行,Spring Boot 会自动装配一个哨兵模式的连接工厂,客户端订阅哨兵通知,主节点切换时自动重连。这是「主挂了客户端知不知道」的关键,不配的话故障转移完成了,客户端还在连老主节点。哨兵本身的部署在 Redis 部署与高可用 那篇。
3. 序列化:默认的 JDK 序列化是个坑
Spring Boot 默认给 RedisTemplate 配的序列化器是 JDK 序列化。存进去的东西在 Redis 客户端里长这样:
\xac\xed\x00\x05t\x00\x06user:1
一串二进制。用 redis-cli 根本没法看,用别的语言也读不了。这不是审美问题,是运维问题——出问题时你连自己写进去的键是什么都看不出来。
所以要改成 JSON。Spring Data Redis 4.x 提供了两个 JSON 序列化器:
JacksonJsonRedisSerializer 写什么读什么必须是什么类型,不会自动带类型信息。反序列化时你得知道类型
GenericJacksonJsonRedisSerializer 会带上类型信息,存进去能还原成原来的对象。写Object类型用它
提示:Jackson 3 的
GenericJacksonJsonRedisSerializer默认不开启 default typing,而 Jackson 2 的GenericJackson2JsonRedisSerializer默认是开的。这是安全上的进步——default typing 能让反序列化端构造任意类型,是历史上多个反序列化 RCE 的入口。新版不用自己去开。
4. 自定义 RedisTemplate
Spring Data Redis 4.0 起换成了 Jackson 3,序列化器的类名跟着变,ObjectMapper 的包名也变了:
Jackson 2(3.x 时代)
com.fasterxml.jackson.databind.ObjectMapper,序列化器叫Jackson2JsonRedisSerializer和GenericJackson2JsonRedisSerializer
Jackson 3(4.x 时代)tools.jackson.databind.ObjectMapper,序列化器叫JacksonJsonRedisSerializer和GenericJacksonJsonRedisSerializer
Jackson 2 的那两个类 从 4.0 起标了@Deprecated,能用但会一直报警告。Jackson 3 的输出格式和 Jackson 2 也不完全一样
还有一处变化:Jackson 2 的 setObjectMapper 方法从 3.0 起就废弃了,现在要通过构造器传进去。
package com.example.redis.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import tools.jackson.annotation.JsonAutoDetect;
import tools.jackson.annotation.PropertyAccessor;
import tools.jackson.databind.ObjectMapper;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
ObjectMapper om = new ObjectMapper();
// 私有字段也要参与序列化,否则 getter 之外的字段会丢
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// Jackson 3 的序列化器只能通过构造器配置 ObjectMapper
JacksonJsonRedisSerializer<Object> json =
new JacksonJsonRedisSerializer<>(om, Object.class);
// key 用字符串序列化,redis-cli 才看得懂
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(json);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(json);
template.afterPropertiesSet();
return template;
}
}
Bean 名字会撞车 Spring Boot 自己也会注册一个叫
redisTemplate的 Bean。你再定义一个同名的,要么启动时报ConflictingBeanDefinitionException,要么注入的时候拿到错的那个。处理办法就一个:注入的时候用@Qualifier指名
别用extends CachingConfigurerSupport这个类在 Spring Framework 6.0 起就废弃了,7.0 里仍然存在但标着@Deprecated(since = "6.0"),官方让直接implements CachingConfigurer
旧数据怎么读 Jackson 3 输出的 JSON 和 Jackson 2 不完全一样。升级期间想读老数据,就临时用GenericJackson2JsonRedisSerializer读,等存量数据迁移完再切
// 注入时用 Qualifier 指名
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate<String, Object> redisTemplate;
5. RedisUtil 工具类
把常用的操作包一层,业务代码就不用每次都写 redisTemplate.opsForValue()。下面是一个精简版,完整的可以按需扩。
package com.example.redis.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/** 写入,不带过期时间 */
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
return false;
}
}
/** 写入并设置过期时间,time 小于等于 0 时按不过期处理 */
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
return set(key, value);
}
return true;
} catch (Exception e) {
return false;
}
}
/** 读取 */
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/** 指定过期时间 */
public boolean expire(String key, long time) {
if (time <= 0) {
return false;
}
redisTemplate.expire(key, time, TimeUnit.SECONDS);
return true;
}
/** 剩余过期时间,返回 0 代表永久有效,-2 代表键不存在 */
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/** 是否存在 */
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/** 删除,支持传一个或多个 key */
public void del(String... keys) {
if (keys == null || keys.length == 0) {
return;
}
if (keys.length == 1) {
redisTemplate.delete(keys[0]);
} else {
redisTemplate.delete((Collection<String>) java.util.Arrays.asList(keys));
}
}
/** 自增 */
public long incr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
/** Hash 存整个对象 */
public void hmset(String key, Map<String, Object> map) {
redisTemplate.opsForHash().putAll(key, map);
}
/** Hash 取整个对象 */
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/** List 存一批 */
public void lpushAll(String key, List<Object> values) {
redisTemplate.opsForList().rightPushAll(key, values);
}
}
实体类要 implements Serializable 不然序列化直接抛异常。不过更推荐的做法是不传实体对象,传 Map 或专门的 DTO,把结构固定住——实体类改个字段名,Redis 里存的老数据就读不出来了
存 Hash 而不是整个对象 一个 key 挂多个 field,读一次HGETALL拿到整个对象,比拆成多个字符串 key 省往返,也方便单独更新某个字段
key 要带业务前缀product:、user:token:,不同业务用不同前缀,方便排查时按前缀筛
工具类造好之后才是重头戏:缓存怎么防穿透和雪崩、锁怎么加、限流怎么限,见 Redis 缓存实战。
6. 什么时候还需要 Jedis
多数情况下不需要——Lettuce 是默认的,也是更安全的选择。确实要换的话,改一个属性加上换依赖,两件事都要做:
spring:
data:
redis:
client-type: jedis
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- 同时要把 Lettuce 排除掉 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
换完之后还有一个差别:LettuceConnectionFactory 实现了连接工厂的销毁语义,会在应用关闭时主动 evict 掉连接池里的连接;JedisConnectionFactory 没有实现这一套。行为上的表现是:Lettuce 的连接在应用关停时被优雅回收,Jedis 的要靠 GC 和操作系统回收。
另外直接用 Jedis API 的场景还有:写 Redis 脚本的测试、需要手动控制事务边界、或者在做一次性的运维脚本。生产代码走 RedisTemplate 就够了。
💡 补充:Spring Data Redis 4.0 还把一批构造函数换成了工厂方法——RedisClusterConfiguration.of(PropertySource) 和 RedisSentinelConfiguration.of(PropertySource),普通构造函数仍然保留。升级时如果看到「找不到构造函数」的编译错误,多半是这里。
🔥 一句话总结:Spring Boot 整合 Redis 的三个必踩点是配置前缀要写 spring.data.redis.*、默认的 JDK 序列化会让 key 变成一串二进制、Jackson 2 的序列化器在 Spring Data Redis 4.x 里已废弃该换 Jackson 3 的新类名。
