Spring Data Redis 是 Spring 生态中操作 Redis 的核心组件,而 RedisTemplate 则是这一组件的操作中枢。但在实际项目中,如果你只是简单地使用 RedisTemplate 默认配置,很快就会发现各种问题:Key 在 Redis 中出现不可读的二进制前缀、数值类型出现异常、JSON 序列化混乱等。本节将从原理到实践,彻底讲清楚 RedisTemplate 的配置、序列化策略和高频操作的最佳实践。
14.2.1 RedisTemplate 的核心体系
Spring Data Redis 对 Redis 客户端的封装并不是简单地屏蔽底层 API,而是在原生连接(Lettuce/Jedis)之上建立了一套操作视图体系,让不同类型的 Redis 命令可以按数据类型归类、按操作方式抽象。
1. 连接工厂与模板
连接工厂是 Redis 接入 Spring 的门户。Spring Boot 默认使用 Lettuce 作为 Redis 客户端,只需在 application.yml 中指定连接信息,即可自动配置 RedisConnectionFactory:
spring:
redis:
host: localhost
port: 6379
password:
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
基于连接工厂,Spring Data Redis 提供了两个关键的操作入口:
RedisTemplate<K, V>:高级操作模板,支持任意类型的 Key 和 Value,负责序列化/反序列化,但使用时需显式指定类型。StringRedisTemplate:继承自RedisTemplate<String, String>,Key 和 Value 均为字符串类型,使用StringRedisSerializer。对于只需要处理字符串缓存的场景,简洁且不易出错。
实际项目中,绝大多数场景下 StringRedisTemplate 足够应对,而当你确实需要存储复杂对象(如实体、Map)时,再去定制 RedisTemplate 的序列化机制。
2. 操作视图分组
RedisTemplate 将 Redis 的五种数据类型包装成对应的操作接口,开发者通过 opsForXxx() 方法获取视图:
opsForValue():字符串与对象操作(ValueOperations)opsForHash():哈希操作(HashOperations)opsForList():列表操作(ListOperations)opsForSet():集合操作(SetOperations)opsForZSet():有序集合操作(ZSetOperations)opsForStream():流操作(StreamOperations,Redis 5.0+)opsForHyperLogLog():基数统计(HyperLogLogOperations)opsForGeo():地理位置(GeoOperations)
这种分组的好处在于,你可以直观地感知当前要操作哪种结构,API 方法也与 Redis 原生命令名高度一致,学习曲线平缓。
14.2.2 序列化策略:决定数据存储形态的关键
许多开发者在使用 RedisTemplate 时遇到诡异问题,根源大多出在序列化机制上。默认情况下,RedisTemplate 的序列化器配置如下:
| 序列化器 | 默认实现 | 默认编码 |
|-------------------------|------------------------------|-------------------|
| keySerializer | JdkSerializationRedisSerializer | JDK 序列化二进制 |
| valueSerializer | JdkSerializationRedisSerializer | JDK 序列化二进制 |
| hashKeySerializer | StringRedisSerializer | UTF-8 字符串 |
| hashValueSerializer | JdkSerializationRedisSerializer | JDK 序列化二进制 |
JdkSerializationRedisSerializer 会将对象序列化为 Java 原生二进制格式,存储在 Redis 中的 Key 会变成形如 \xAC\xED\x00\x05t\x00\x05myKey 的字节序列,不仅肉眼无法阅读,而且当非 Java 客户端访问时会直接报错。生产环境下,必须显式替换序列化策略。
1. 常用的序列化器
StringRedisSerializer:将字符串以 UTF-8 字节存储,简洁可读。建议用于所有 Key 和字符串类型的 Value。GenericJackson2JsonRedisSerializer:通过 Jackson 将对象序列化为 JSON 字符串,并在 JSON 中嵌入@class字段以支持反序列化时的类型恢复。优点是通用性强,缺点是存在类型耦合和存储开销。Jackson2JsonRedisSerializer:仅序列化指定类型的对象为 JSON,不存储类型信息,需要在使用时指定Class。适合同一类型对象读写,性能更好。GenericToStringSerializer:将对象通过ConversionService转为字符串,适用简单数值类型但灵活性有限。FastJsonRedisSerializer(第三方):使用阿里的 FastJSON,速度较快,但社区版本有过安全漏洞,需谨慎使用。
2. 实战配置:构建清晰可维护的 RedisTemplate
最稳健的方案是:Key 一律用字符串,Value 根据场景选择字符串或 JSON。以下配置覆盖了核心场景,并设计了一个通用的 FastJson2JsonRedisSerializer(假设项目使用 FastJSON2,速度与安全兼顾):
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key 序列化
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// Value 序列化
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
如果你希望 Value 不存储类型信息,可以使用 Jackson2JsonRedisSerializer 并指定 Object.class:
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(
mapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL
);
serializer.setObjectMapper(mapper);
// 设置为 valueSerializer
3. 默认行为演示带来的教训
为了让你更深刻地认识到序列化配置的重要性,我们看一个不加配置的对比。假设使用默认 RedisTemplate 存储一个用户对象:
redisTemplate.opsForValue().set("user:1001", new User("张三", 25));
执行后在 Redis 中查看 key,会看到:
\xAC\xED\x00\x05t\x00\x09user:1001
这种存储会导致两个严重问题:第一,Redis 内存用尽排查困难;第二,其他客户端无法读取。上线前的第一步,就是统一序列化方案。
14.2.3 常用操作与最佳实践
在实际业务开发中,Redis 的使用往往高度集中于缓存、计数、锁和队列等几个典型场景。下面通过案例展示最常用的操作及其注意事项。
1. 字符串操作 —— 缓存与反哺缓存穿透
字符串操作是 Redis 中最基础也是最常用的功能。通过 StringRedisTemplate 可以直接存取字符串:
stringRedisTemplate.opsForValue().set("cache:hot:productList", listJson, Duration.ofMinutes(10));
String cachedJson = stringRedisTemplate.opsForValue().get("cache:hot:productList");
if (cachedJson == null) {
// 查询数据库,将结果写入缓存,注意设置合理的过期时间
} else {
// 解析JSON返回
}
批量操作:当需要一次获取多个 Key 对应的值时,应使用 multiGet 减少网络往返次数:
List<String> keys = Arrays.asList("key1", "key2", "key3");
List<String> values = stringRedisTemplate.opsForValue().multiGet(keys);
increment 用于计数/限流:increment 是原子操作,常用于商品库存扣减、接口调用频次限制等:
Long count = stringRedisTemplate.opsForValue().increment("counter:api:invoke:user123");
if (count == 1) {
stringRedisTemplate.expire("counter:api:invoke:user123", 60, TimeUnit.SECONDS);
}
if (count > 100) {
throw new RuntimeException("调用频率超限");
}
2. 哈希操作 —— 对象缓存的最佳载体
当需要缓存一个包含多个字段的对象且希望部分字段更新时,Hash 结构比整体序列化字符串更高效。以缓存用户详情为例:
HashOperations<String, String, Object> hashOps = stringRedisTemplate.opsForHash();
String userKey = "user:detail:1002";
// 存储多个字段
Map<String, String> userMap = new HashMap<>();
userMap.put("name", "李四");
userMap.put("age", "30");
userMap.put("city", "杭州");
hashOps.putAll(userKey, userMap);
// 单独更新某个字段
hashOps.put(userKey, "city", "北京");
// 获取指定字段
String name = (String) hashOps.get(userKey, "name");
// 批量获取多个字段
List<Object> fields = hashOps.multiGet(userKey, Arrays.asList("name", "age"));
// 获取整个哈希的所有键值对
Map<String, Object> allFields = hashOps.entries(userKey);
3. 列表操作 —— 简单的消息队列与最新数据展示
列表可以用作轻量级的消息队列,或存储时间线类型的数据(如最新评论)。左进右出可以实现先进先出:
ListOperations<String, String> listOps = stringRedisTemplate.opsForList();
String listKey = "queue:order_event";
// 生产者:从左侧推入
listOps.leftPush(listKey, "OrderEvent:1003");
// 消费者:从右侧弹出,阻塞等待(无数据时返回null)
String event = listOps.rightPop(listKey, 10, TimeUnit.SECONDS);
为了避免列表长度失控,产品环境必须配合 trim 限制最大长度:
listOps.leftPush(listKey, "newest msg");
listOps.trim(listKey, 0, 999); // 保留最新的1000条
此外,range 方法可用于分页获取列表数据:
List<String> recentMsgs = listOps.range(listKey, 0, 9); // 获取最新10条
4. 集合与有序集合 —— 去重与排行榜
集合适用于标签、去重列表等场景。有序集合则凭借分数排序,天然适合做排行榜:
// 集合:标签系统
SetOperations<String, String> setOps = stringRedisTemplate.opsForSet();
setOps.add("product:tags:2001", "热销", "新品", "包邮");
// 判断是否在集合中
Boolean isHot = setOps.isMember("product:tags:2001", "热销");
// 有序集合:销量排行榜
ZSetOperations<String, String> zSetOps = stringRedisTemplate.opsForZSet();
zSetOps.add("ranking:sales:daily", "product:3001", 1580);
zSetOps.add("ranking:sales:daily", "product:3002", 920);
// 获取销量前10的商品(分数从高到低)
Set<String> topProducts = zSetOps.reverseRange("ranking:sales:daily", 0, 9);
// 获取某个商品的排名(0开始)
Long rank = zSetOps.reverseRank("ranking:sales:daily", "product:3001");
5. 过期时间与管道
Redis 是内存数据库,过期时间管理是防止内存溢出的生命线:
// 设置key时同时指定过期时间
stringRedisTemplate.opsForValue().set("code:123456", "a1b2c3", Duration.ofMinutes(5));
// 对已有key设置过期
stringRedisTemplate.expire("code:123456", 10, TimeUnit.MINUTES);
当需要执行大量写入操作时,可以使用 executePipelined 将多个命令打包一次性发送,大幅减少网络往返时间:
List<Object> results = stringRedisTemplate.executePipelined(
(RedisCallback<Object>) connection -> {
for (int i = 0; i < 1000; i++) {
connection.set(("pipKey:" + i).getBytes(), "value".getBytes());
}
return null;
}
);
14.2.4 常见问题与陷阱
1. 大 Key 问题:将巨大的对象序列化为字符串存入 Redis 会导致内存浪费和网络阻塞。尽量拆分为 Hash 或使用压缩算法。
2. 序列化不一致:读写的 RedisTemplate 必须使用相同的序列化策略。如果写入时用 Jackson 序列化,读取时却用 StringRedisTemplate,会抛出异常。
3. 事务与原子性:RedisTemplate 支持 multi/exec 事务,但 Redis 的事务没有回滚能力,更推荐使用 Lua 脚本实现原子性操作。
4. 连接泄漏与超时:使用 execute 回调时,务必正确释放连接。Spring 通常会自动管理,但在手动获取连接时必须小心。
5. 缓存穿透与雪崩:在 Redis 层之上,Spring Cache 抽象可以配合 @Cacheable 的 sync 属性和空值缓存策略规避。更细节的方案在下一节结合 Spring Cache 深入讨论。
通过掌握 RedisTemplate、序列化策略及常用操作,你已经获得了直接控制 Redis 的全部能力。但 Spring 并未止步于此,它还提供了更上层的缓存抽象——Spring Cache,将缓存操作进一步声明化,让缓存管理脱离对 Redis API 的直接依赖,这也正是我们下一节要探讨的主题。