艾体宝干货|【Redis实用技巧#14】不止 @Cacheable,Spring Boot 中真正拉开差距的 4 个 Redis 实战模式

3 阅读5分钟

在 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 项目中还缺少以上某一环,那它迟早会在流量上来时暴露问题。

缓存从来不是优化细节,它是架构的一部分。