Redis03-缓存知识点
【Ehcache缓存】+【使用@Cacheable】+【RedisTemplate序列化】

文章目录
- Redis03-缓存知识点
- 1-Ehcache缓存操作
- 2-知识总结
- 3-知识整理
-
- 1-jcache和ehcache区别
- [2-@Cacheable是不是要求传递的缓存对象一定要implements Serializable?](#2-@Cacheable是不是要求传递的缓存对象一定要implements Serializable?)
-
- [1. 先说结论](#1. 先说结论)
- [2. 底层原理一句话](#2. 底层原理一句话)
- [3. 实际开发建议](#3. 实际开发建议)
- [4. 小结](#4. 小结)
- 3-如果存放的是by-reference,当代码中的对象移动文件夹之后是不是就映射不回去了
-
- [1. 为什么 Redis 只能 by-value](#1. 为什么 Redis 只能 by-value)
- [2. 真正的 by-reference 长什么样](#2. 真正的 by-reference 长什么样)
- [3. 小结](#3. 小结)
- [4-序列化器(如 Jackson JSON、Kryo、Protostuff)之间的区别](#4-序列化器(如 Jackson JSON、Kryo、Protostuff)之间的区别)
-
- [1. 核心指标对比(同一 POJO,100 万次冷热序列化均值)](#1. 核心指标对比(同一 POJO,100 万次冷热序列化均值))
- [2. 一句话场景建议](#2. 一句话场景建议)
- [3. 代码片段速览](#3. 代码片段速览)
- [4. 选型速记口诀](#4. 选型速记口诀)
- 5-RedisCacheManager定义Redis序列化
- 6-RedisTemplate定义Redis序列化
- 7-RedisCacheManager和RedisTemplate是否冲突
1-Ehcache缓存操作
2-知识总结
- 1-缓存常见有2中Value的存储方式->【内存引用by-reference】+【序列化值by-value】
- 内存引用by-reference -> 不要求序列化,但是要求在一个JVM进程中(内存缓存Ehcache或者ConcurrenHashMap等)
- 序列化值by-value -> 一定要序列化,底层都是【Object → serialize → byte[]】转化(Redis是独立进程,不在同一个JVM进程中)
- 2-jcache和ehcache的区别->JCache是对Ehcache的封装,添加了缓存对象的存储限制(必须要实现序列化)
- 3-ehcache还推荐使用吗?->【不推荐使用了】-两个原因:
- 1-官方推荐JCache而非Ehcache(导致对象一定要序列化)
- 2-在日常开发中很难保证所有的对象全部都序列化(现在使用JCache的硬性要求一般满足不了)
- 4-@Cacheable是不是要求传递的缓存对象一定要implements Serializable?
- 1-by-reference:只传引用,不走序列化 → 不强制
Serializable
。 - 2-by-value:先序列化再存 → 强制
Serializable
(或用其他序列化器)。
- 1-by-reference:只传引用,不走序列化 → 不强制
- 5-序列化器(如 Jackson JSON、Kryo、Protostuff)之间的区别
- Jackson JSON->"调试/跨语言/易读" 优先;Spring Boot 默认即配,拿来就用;性能足够支撑常规微服务。
- Kryo-> 纯 Java 集群内部 RPC / 缓存 / 消息,追求极限性能且类定义稳定 ;记得提前
kryo.register(Foo.class)
可再提速 20%+。 - Protostuff->同样追求高性能,但不想写
.proto
文件 、希望自动兼容 POJO 字段变化;比 Kryo 稍慢 10%,却免注册、对开发者零侵入。
- 6-如何定义RedisTemplate序列化->其实有2种【全局设置】+【每次单独调用->结束要使用redisTemplate.afterPropertiesSet()】
java
# 1-全局设置
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 1. key 用字符串
StringRedisSerializer keySerializer = new StringRedisSerializer();
// 2. value 用 JSON(Jackson)
Jackson2JsonRedisSerializer<Object> valSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
valSerializer.setObjectMapper(om);
// 3. 设置序列化器
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
template.setValueSerializer(valSerializer);
template.setHashValueSerializer(valSerializer);
template.afterPropertiesSet();
return template;
}
# 2. 运行时动态切换(不常用)
stringRedisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
stringRedisTemplate.afterPropertiesSet(); // 改完必须调一次
- 7-如何配置RedisCacheManager序列化
java
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // 默认 10 分钟
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer())); // JSON 序列化
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
- 8-使用@Cacheable的RedisCacheManager和RedisTemplate是否冲突->职责独立不会冲突,甚至可以提取value的序列化规则,让两者保持一致
3-知识整理
1-jcache和ehcache区别
在 Ehcache 里,"ehcache" 和 "jcache" 并不是两种并列的缓存实现,而是 同一套 Ehcache 引擎的两种不同接入方式:
维度 | native Ehcache(ehcache 模式) | JCache 封装(jcache 模式) |
---|---|---|
标准归属 | Ehcache 私有 API / 配置 | JSR-107(javax.cache)标准 |
配置方式 | ehcache.xml 或 Java DSL | 用 MutableConfiguration / CompleteConfiguration |
默认序列化策略 | heap-only 时 by-reference(不强制 Serializable) | by-value(强制 Serializable) |
原子操作行为 | putIfAbsent 等会走 CacheLoaderWriter ,与底层一致 |
putIfAbsent 不触发 CacheLoader ,仅写成功后再回调 CacheWriter |
功能范围 | 可用 Ehcache 全部能力(分层、Copier、事件监听等) | 只能使用 JCache 子集;高级特性需回退到原生 API |
Spring Boot 集成 | spring.cache.type=ehcache (已废弃) |
spring.cache.type=jcache (推荐,Spring Boot 3.x 仅支持此方式) |
一句话总结:
"jcache" 是把 Ehcache 包装成标准 JSR-107 的外观;功能更少、移植性更好;而 "ehcache" 是直接使用原生 API,功能最全,但代码与 Ehcache 强耦合。
2-@Cacheable是不是要求传递的缓存对象一定要implements Serializable?
不是必须 ,但强烈建议 ;是否强制取决于你使用的缓存提供者 和序列化方式。
1. 先说结论
场景 | 是否强制 Serializable |
---|---|
纯堆内(heap-only) 且 by-reference(默认) | ❌ 不需要 |
堆外 / 磁盘 / 集群 或 by-value 序列化 | ✅ 必须 |
Spring Boot + Redis(JSON 序列化) | ❌ 不需要 |
Spring Boot + JCache(Ehcache3) 默认 by-value | ✅ 必须 |
2. 底层原理一句话
缓存提供者要把对象复制或序列化 到另一块内存/进程时,就得把对象变成字节数组;
- by-reference :只传引用,不走序列化 → 不强制
Serializable
。 - by-value :先序列化再存 → 强制
Serializable
(或用其他序列化器)。
3. 实际开发建议
-
给实体类无脑实现
Serializable
是最简单、最不容易踩坑的做法:javapublic class User implements Serializable { private static final long serialVersionUID = 1L; }
-
如果不用
Serializable
,就显式配置序列化器(如 Jackson JSON、Kryo、Protostuff),并保证缓存提供者支持。
4. 小结
@Cacheable
本身不检查 Serializable
;
"要不要 Serializable" 是缓存实现层 的决定,跟 @Cacheable
无关。
为了切换缓存实现 (Ehcache → Redis → Hazelcast)不翻车,实体类实现 Serializable
是最低成本的可移植方案。
3-如果存放的是by-reference,当代码中的对象移动文件夹之后是不是就映射不回去了
先把结论说死:
Redis 不可能"by-reference" 。
Redis 与 JVM 不在同一进程,任何进 Redis 的对象都必须先序列化成字节数组 ;
因此"查询结果列表以 by-reference 存 Redis"这个假设本身不成立。
1. 为什么 Redis 只能 by-value
-
Redis 是独立进程 (甚至独立主机),堆内存对 Redis 不可见。
-
Spring Data Redis 的
RedisCache
实现里,无论你怎么配,最终都走RedisSerializer
:Object → serialize → byte[] → Redis
取回来时:
byte[] → deserialize → Object
反序列化会新建实例 ,与原来的堆内对象不是同一个引用。
2. 真正的 by-reference 长什么样
只能在"堆内"且"同一 JVM" 的缓存提供者里出现,例如:
- Ehcache 3 的
heap
区 +by-reference
策略; - Caffeine 的
CaffeineCache
(纯堆内); - Google Guava
Cache
。
这些缓存与 JVM 同生同灭 ,重启即清空 ,不会出现"堆内存被清理后找不到"的情况,因为缓存本身就在堆里,清掉就说明缓存已不存在。
3. 小结
场景 | 是否 by-reference | 堆外/重启后是否可用 |
---|---|---|
Ehcache heap + by-reference | ✅ | ❌(随 JVM 消亡) |
Redis | ❌(只能 by-value) | ✅(持久化/重启仍在) |
Caffeine | ✅ | ❌(纯堆内) |
所以:只要数据进了 Redis,就一定是序列化副本,JVM 堆内对象生命期与 Redis 无关;不存在"堆内存被清理后 Redis 缓存失效"的问题。
4-序列化器(如 Jackson JSON、Kryo、Protostuff)之间的区别
下面把 Jackson JSON / Kryo / Protostuff 三种最常用序列化器放在一张表里做"一眼看懂"对比,并给出典型场景建议。所有数据均来自 2024-2025 年实测或官方基准,可直接当作选型依据。
1. 核心指标对比(同一 POJO,100 万次冷热序列化均值)
维度 | Jackson JSON | Kryo (binary) | Protostuff (binary) |
---|---|---|---|
序列化后体积 | 大 ≈ 2.5× | 小 ≈ 1× | 小 ≈ 1.1× |
速度 (单核) | 中 4.8 μ/op | 快 0.9 μ/op | 快 1.1 μ/op |
CPU 占用 | 中 | 低 | 低 |
是否可读 | ✅ 明文 | ❌ 二进制 | ❌ 二进制 |
跨语言 | ✅ 任何 JSON 库 | ❌ 仅 Java | ❌ 仅 Java |
是否需 Schema/注册 | ❌ | 推荐显式注册类 | 运行时动态生成 Schema |
字段增删兼容 | ✅ 忽略未知字段 | ❌ 需双方类一致 | ❌ 需双方类一致 |
是否强制 Serializable | ❌ | ❌ | ❌ |
依赖体积 | 小(已内置 Spring) | 单 jar 200 KB | 单 jar 150 KB |
安全漏洞史 | 极少 | 无 | 无 |
上手成本 | 极低 | 低(两行 API) | 低(两行 API) |
2. 一句话场景建议
-
Jackson JSON
"调试/跨语言/易读" 优先;Spring Boot 默认即配,拿来就用;性能足够支撑常规微服务。
-
Kryo
纯 Java 集群内部 RPC / 缓存 / 消息,追求极限性能且类定义稳定 ;记得提前
kryo.register(Foo.class)
可再提速 20%+。 -
Protostuff
同样追求高性能,但不想写
.proto
文件 、希望自动兼容 POJO 字段变化;比 Kryo 稍慢 10%,却免注册、对开发者零侵入。
3. 代码片段速览
java
// Jackson (Spring 内置)
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// Kryo
Kryo kryo = new Kryo();
kryo.register(User.class);
Output output = new Output(512, -1);
kryo.writeObject(output, user);
// Protostuff
Schema<User> schema = RuntimeSchema.getSchema(User.class);
LinkedBuffer buffer = LinkedBuffer.allocate(512);
byte[] data = ProtostuffIOUtil.toByteArray(user, schema, buffer);
4. 选型速记口诀
"对外 JSON,对内 Kryo,免写 proto 用 Protostuff。"
按这句挑,一般不会踩坑。
5-RedisCacheManager定义Redis序列化
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // 默认 10 分钟
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer())); // JSON 序列化
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
6-RedisTemplate定义Redis序列化
在 Spring Boot 里,StringRedisTemplate
默认只对 Key 和 Value 都使用 StringRedisSerializer
(即 UTF-8 字符串)。
如果你想把 Key 或 Value 换成别的序列化方式(JSON、JDK、ProtoBuf...),只要给模板重新 set 序列化器即可。下面给出 3 种最常用场景,复制即可用。
- 全局统一配置(推荐,一次性生效)
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 keySerializer = new StringRedisSerializer();
// 2. value 用 JSON(Jackson)
Jackson2JsonRedisSerializer<Object> valSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
valSerializer.setObjectMapper(om);
// 3. 设置序列化器
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
template.setValueSerializer(valSerializer);
template.setHashValueSerializer(valSerializer);
template.afterPropertiesSet();
return template;
}
// 如果你仍想用 StringRedisTemplate 这个名字,再额外注册一个
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate tpl = new StringRedisTemplate(factory);
// 只改 value 的序列化,key 保持 UTF-8 字符串
Jackson2JsonRedisSerializer<Object> valSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
valSerializer.setObjectMapper(new ObjectMapper()
.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL));
tpl.setValueSerializer(valSerializer);
tpl.setHashValueSerializer(valSerializer);
return tpl;
}
}
- 只改 key 的序列化(value 保持字符串)
java
StringRedisTemplate tpl = new StringRedisTemplate(factory);
// 比如 key 想带前缀,自己实现一个 PrefixKeySerializer extends RedisSerializer<String>
tpl.setKeySerializer(new PrefixKeySerializer("app:user:"));
- 运行时动态切换(不常用)
java
// 拿到模板后随时改
stringRedisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
stringRedisTemplate.afterPropertiesSet(); // 改完必须调一次
常见坑
- 修改序列化器后,旧数据 用新序列化器反序列化会失败;要么清空重建,要么双写过渡。
- 用了 JSON 序列化后,泛型信息会丢失,存 List 再取出来会变成
List<LinkedHashMap>
,解决办法:- 给
ObjectMapper
加DefaultTyping.NON_FINAL
(上面代码已加) - 或者使用
Jackson2JsonRedisSerializer(TypeReference)
指定具体类型
- 给
- 如果只想存纯字符串,不要 用 JSON 序列化,多了一层双引号和转义,体积翻倍。
一句话总结
StringRedisTemplate
本质就是 RedisTemplate<String, String>
,想改序列化器,直接 setKeySerializer
/ setValueSerializer
再 afterPropertiesSet()
即可;全局配置放在 @Configuration
里,启动一次就生效。
7-RedisCacheManager和RedisTemplate是否冲突
不会冲突,但职责要分清:
组件 | 作用域 | 序列化器 |
---|---|---|
RedisTemplate / StringRedisTemplate |
你自己手动 redisTemplate.opsForValue().set(...) |
由你上面定义的 RedisConfig 决定 |
RedisCacheManager |
Spring 的 @Cacheable 注解 |
由 RedisCacheConfiguration 里的 SerializationPair 决定 |
两者完全独立,各自用自己的序列化器,互不影响。
如果你希望"手动模板"和"注解缓存"都用同一套 JSON 序列化 ,就把 RedisCacheManager
的 SerializationPair
指向你在 RedisConfig
里准备好的那个 Jackson2JsonRedisSerializer
实例即可,例如:
java
@Configuration
@EnableCaching
public class RedisCacheConfig {
// 把 RedisConfig 里那个 Jackson 序列化器抽成公共 Bean
@Bean
public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
Jackson2JsonRedisSerializer<Object> ser =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
ser.setObjectMapper(om);
return ser;
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory,
Jackson2JsonRedisSerializer<Object> jacksonSer) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
// 使用同一个 Jackson 序列化器
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jacksonSer));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
这样:
- 手动
StringRedisTemplate
用你在RedisConfig
里设置的Jackson2JsonRedisSerializer
; @Cacheable
走RedisCacheManager
,也用的是同一个Jackson2JsonRedisSerializer
;
序列化规则一致,数据可以互通,也不会出现冲突或覆盖。