人人都会AI编程

14.2 Redis 深度整合:RedisTemplate、序列化策略、常用操作

更新时间:2026-07-10

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 抽象可以配合 @Cacheablesync 属性和空值缓存策略规避。更细节的方案在下一节结合 Spring Cache 深入讨论。

通过掌握 RedisTemplate、序列化策略及常用操作,你已经获得了直接控制 Redis 的全部能力。但 Spring 并未止步于此,它还提供了更上层的缓存抽象——Spring Cache,将缓存操作进一步声明化,让缓存管理脱离对 Redis API 的直接依赖,这也正是我们下一节要探讨的主题。