在 Spring Boot 中使用 Redis,很多新手的第一反应就是加上 @Cacheable。的确,只需短短一行注释,就能看到数据库负载显著下降,甚至能让接口响应速度提升一个数量级。
但在分布式系统的实战领域,仅仅会加注解是远远不够的。如果你不了解序列化、TTL(过期时间)和主从复制,那么盲目写下的每一行注解,都可能变成线上事故的导火索。
这样的例子并不罕见:因为滥用注解,Redis 内存被瞬间撑爆,用户会话丢失,API 只能返回三天前的数据。在 redis-cli 中看到的 Key 全是乱码,没人能解释发生了什么。
初级开发懂得用注解,而资深开发要懂得管理缓存。 想要让缓存方案真正具备生产力,必须掌握以下四种核心模式。
一、序列化策略:让缓存可读、可演进、可维护
问题本质:默认 Java 序列化是隐患
Spring Boot 默认使用 JdkSerializationRedisSerializer。 问题在于:
- 键和值都是二进制格式,
redis-cli里像乱码 - 类名、字段一旦重构,反序列化直接失败
- 数据体积大,浪费内存
- 排查线上问题几乎不可读
例如默认配置:
@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);return template; // 默认 JDK 序列化}
在 Redis 中看到的可能是:
\xAC\xED\x00\x05t\x00\x08products
这对排障没有任何帮助。
正确做法:Key 用字符串,Value 用 JSON
@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());return template;}
此时 Redis 中的数据:
Key: "products:123"
Value: {"id":123,"name":"Widget","price":29.99}
非常明确:
- 可直接用
redis-cli查看 - JSON 对类重构更友好
- 体积更小
- 更适合跨语言场景
缓存不是黑盒。如果连 Redis 里实际存的是什么都不知道,就无法调试生产问题。
二、TTL 与淘汰策略:避免“缓存炸内存”
常见误区:永不过期
@Cacheable("products")public Product findById(Long id) {return repository.findById(id).orElseThrow();}
问题:
- 无 TTL → 数据永久存在
- 数据更新 → 旧值长期滞留
- 内存打满 → Redis 随机淘汰 key
- Session 可能被误删
这不是缓存,这是定时炸弹。
正确做法一:显式设置 TTL
@Beanpublic CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config =
RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10)).serializeKeysWith(...).serializeValuesWith(...);return RedisCacheManager.builder(factory).cacheDefaults(config).build();}
不同数据不同 TTL:
Map<String, RedisCacheConfiguration> configs = Map.of("products", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(5)),"categories", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(24)));
这是对数据新鲜度的理解体现:
- 高频变更数据 → 短 TTL
- 基础字典数据 → 长 TTL
正确做法二:配置内存淘汰策略
在 redis.conf 或 Spring 配置中:
maxmemory 256mb
maxmemory-policy allkeys-lru
allkeys-lru 表示:
当内存达到上限时,在所有 key 中淘汰“最近最少使用”的数据。
如果不配置:
- 默认策略可能不符合业务需求
- 甚至直接拒绝写入
TTL 控制“数据多久过期”。
淘汰策略控制“内存满了怎么办”。
这两者是不同层级的问题。
三、主从复制:让读流量可横向扩展
单机 Redis 的局限
new LettuceConnectionFactory(new RedisStandaloneConfiguration("localhost", 6379));
问题:
- 所有读写打到一台机器
- 高读流量时成为瓶颈
- 单点故障
在高并发系统里,这不可接受。
使用主从复制 + 读写分离
LettuceClientConfiguration clientConfig =
LettuceClientConfiguration.builder().readFrom(ReadFrom.REPLICA_PREFERRED).build();
RedisStandaloneConfiguration serverConfig =new RedisStandaloneConfiguration("redis-master", 6379);return new LettuceConnectionFactory(serverConfig, clientConfig);
机制:
- 写 → Master
- 读 → 优先 Replica
- Replica 不可用 → 回退 Master
这带来:
- 读流量水平扩展
- 更高可用性
- 更好的资源利用率
需要理解的分布式问题
- 主从复制是异步复制
- 可能存在短暂数据延迟
- 读到旧值是正常现象
如果业务对强一致性敏感:
- 不要随意读从库
- 或设计幂等与重试机制
核心认知升级:
缓存扩展不只是加机器,而是理解复制语义和一致性模型。
四、条件缓存与精细化失效:避免“缓存污染”
常见错误 1:缓存 null
@Cacheable("products")public Product findById(Long id) {return repository.findById(id).orElse(null);}
问题:
- 不存在的商品被缓存为 null
- 后续创建成功 → 仍返回 null
- 典型缓存污染
正确做法:使用 unless
@Cacheable(
value = "products",
key = "#id",
unless = "#result == null")
避免无效数据进入缓存。
常见错误 2:更新时清空整个缓存
@CacheEvict(value = "products", allEntries = true)
更新一个商品,清空所有商品缓存。
这会导致瞬间缓存穿透数据库。
正确做法:精准失效
@CacheEvict(value = "products", key = "#product.id")
或者使用 @CachePut:
@CachePut(value = "products", key = "#product.id")
更新数据库后立即更新缓存,避免下一次读取产生 cache miss。
多缓存一致性
@Caching(evict = {@CacheEvict(value = "products", key = "#product.id"),@CacheEvict(value = "productsByCategory", key = "#product.category.id")})
这体现的是:
对缓存依赖关系的建模能力。
从“加注解”到“理解系统”
对比一下两种思维方式。
初级方式
@Cacheable("products")
- 不知道序列化方式
- 不设 TTL
- 不懂淘汰策略
- 不考虑复制
- 不控制 null
- 更新全清
缓存只是“减少数据库查询”。
生产级方式
- JSON 序列化
- 显式 TTL
- allkeys-lru
- 主从读写分离
- 条件缓存
- 精准失效
- 理解复制延迟
缓存不再是一个注解,而是一套数据生命周期管理机制。
本质区别在哪里?
区别不在于会不会写:
@Cacheable
而在于是否理解:
- Redis 内部如何存储数据
- 内存如何增长
- key 如何被淘汰
- 主从如何复制
- 更新如何传播
- 一致性如何保证
真正成熟的工程师关注的不是“是否用了缓存”,
而是:
当缓存出问题时,我是否知道该去哪里排查?
如果你的 Spring Boot 项目中还缺少以上某一环,那它迟早会在流量上来时暴露问题。
缓存从来不是优化细节,它是架构的一部分。