Spring Cache + Redis 最佳实践指南

186 阅读8分钟

Spring Cache + Redis 最佳实践指南,旨在帮助你构建高性能、稳定且可维护的缓存方案:

核心原则:

  1. 缓存是优化,不是数据源: 始终确保数据库是数据的唯一真实来源。缓存可能失效、被清除或包含过期数据。
  2. 明确缓存目的: 清晰定义缓存什么(热点数据、复杂计算结果)、为什么缓存(减轻DB负载、加速响应)以及缓存的粒度(对象、DTO、部分对象)。
  3. 理解 Cache Abstraction: Spring Cache 是抽象层,提供统一的编程模型(@Cacheable, @CachePut, @CacheEvict)。具体实现(如 Redis)通过 CacheManagerCache 接口接入。

最佳实践详解:

1. 依赖与配置

  • 选择库:

    • 推荐: spring-boot-starter-data-redis (默认使用 Lettuce 客户端)。Lettuce 基于 Netty,性能好,支持异步和响应式。
    • 可选:Jedis (成熟但线程模型稍旧,连接池需仔细管理)。
  • 连接池 (Lettuce):

    • 必须配置: Lettuce 默认使用无界连接池,生产环境必须配置连接池限制 (spring.redis.lettuce.pool.*) 以避免资源耗尽。
    • 合理参数: 根据并发量和 Redis 服务器性能设置 max-active, max-idle, min-idle, max-wait
  • 序列化器 (Critical!):

    • 避免 JDK 序列化 (JdkSerializationRedisSerializer): 慢、体积大、不安全、语言绑定强。
    • 首选 JSON:
      • GenericJackson2JsonRedisSerializer: 最常用。存储类名信息,反序列化方便。注意处理复杂类型(如 LocalDateTime)和循环引用。
      • Jackson2JsonRedisSerializer: 需要显式指定目标类型 (new Jackson2JsonRedisSerializer<>(MyClass.class))。不存储类名,更节省空间,但类型信息需应用层管理。
    • 其他可选: Protobuf, Kryo (需自定义配置,性能好但兼容性/可读性稍差)。
    • Value 序列化器:RedisTemplateRedisCacheManager 显式设置合适的 value 序列化器。
    • Key 序列化器: 推荐 StringRedisSerializer。Key 必须是可读、一致且可预测的字符串(Spring Cache 默认生成策略通常够用)。
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        // Key 序列化器
        template.setKeySerializer(RedisSerializer.string());
        // HashKey 序列化器 (通常也建议用 String)
        template.setHashKeySerializer(RedisSerializer.string());
        // Value 序列化器 - 使用 JSON
        template.setValueSerializer(RedisSerializer.json());
        // HashValue 序列化器 - 使用 JSON
        template.setHashValueSerializer(RedisSerializer.json());
        return template;
    }
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string())) // Key: String
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json())); // Value: JSON
    
        // 可以在此基于 cache name 进行个性化配置 (如 TTL)
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(config)
                .build();
    }
    

2. 缓存策略与注解使用

  • @Cacheable (查询):
    • value/cacheNames: 指定缓存名称(逻辑分区)。
    • key: 使用 SpEL 表达式精确控制缓存键生成。确保唯一性(参数组合)。避免过度复杂表达式。
    • condition/unless: 使用 SpEL 进行条件缓存(例如,只缓存某些状态的数据、结果不为空的)。
    • sync (Spring 4.3+): 在缓存未命中时,对相同 key 的并发请求进行同步,防止缓存击穿(仅一个线程执行方法,其他线程等待结果)。强烈建议在高并发场景下开启。
  • @CachePut (更新):
    • 用于更新缓存。方法总是执行,并将结果放入缓存。
    • 确保 key@Cacheable@CacheEvict 使用的 key 一致,以更新正确的条目。
  • @CacheEvict (删除):
    • key/allEntries: 删除单个条目或清空整个 cacheNames 分区。allEntries = true谨慎使用,尤其是在大缓存分区。
    • beforeInvocation: 设置为 true 在执行方法清除缓存(例如,在更新/删除操作前清除,确保后续读操作不会拿到旧数据)。默认(false)在方法成功执行后清除。
  • @Caching: 组合多个缓存操作。
  • @CacheConfig: 在类级别配置公共的 cacheNames, keyGenerator, cacheManager 等,避免在每个方法上重复。

3. 缓存有效期 (TTL - Time To Live)

  • 必须设置 TTL: 防止永久失效数据占用内存。Redis 原生支持 EXPIRE

  • RedisCacheConfiguration 中设置默认 TTL:

    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(30)) // 设置默认全局 TTL 为 30 分钟
        .serializeKeysWith(...)
        .serializeValuesWith(...);
    
  • 按缓存名称 (cacheNames) 定制 TTL:

    Map cacheConfigurations = new HashMap<>();
    // 为名为 "users" 的缓存设置 1 小时 TTL
    cacheConfigurations.put("users", RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofHours(1)));
    // 为名为 "products" 的缓存设置 10 分钟 TTL
    cacheConfigurations.put("products", RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(10)));
    
    return RedisCacheManager.builder(redisConnectionFactory)
        .cacheDefaults(config) // 默认配置
        .withInitialCacheConfigurations(cacheConfigurations) // 特定缓存配置
        .build();
    
  • TTL 策略考虑:

    • 数据变更频率: 变更频繁的设短 TTL。
    • 数据重要性/实时性要求: 要求高的设短 TTL 或结合主动失效 (@CacheEvict)。
    • 缓存穿透风险: 非常短的 TTL 可能增加缓存穿透风险(结合缓存空值或布隆过滤器)。
    • 随机抖动: 在固定 TTL 上增加小范围随机值(如 baseTTL + random.nextInt(300)),避免大量缓存同时失效导致雪崩。

4. 处理缓存问题

  • 缓存穿透 (Cache Penetration):
    • 问题: 大量请求查询数据库中根本不存在的数据(如无效 ID),绕过缓存直达 DB。
    • 解决方案:
      • 缓存空值 (Null Object): 使用 @Cacheableunless 条件或 AOP 拦截,将查询结果为 null 或特定“空对象”也缓存起来(设置一个较短的 TTL,如 2-5 分钟)。
      • 布隆过滤器 (Bloom Filter): 在查询缓存/DB 前,先用内存中的布隆过滤器判断 key 可能不存在(快速失败)。适用于 key 空间有限且可预知的场景。Redis 4.0+ 支持 BF.ADD/BF.EXISTS 模块。
  • 缓存击穿 (Cache Breakdown):
    • 问题: 某个热点 key 过期瞬间,大量并发请求同时击穿缓存,直达 DB。
    • 解决方案:
      • @Cacheable(sync = true) Spring 提供的本地互斥锁,是首选方案
      • 分布式锁 (复杂): 在缓存失效时,使用 Redis SETNX 或 Redisson 等库获取分布式锁,确保只有一个线程去重建缓存。慎用,容易引入性能瓶颈和复杂度。
      • 永不过期 + 后台刷新 (逻辑过期): 缓存不设 TTL,但存储一个逻辑过期时间字段。应用层发现逻辑过期时,触发异步线程去刷新缓存。需要额外代码维护。
  • 缓存雪崩 (Cache Avalanche):
    • 问题: 大量 key 同时失效Redis 实例宕机,导致所有请求涌向 DB。
    • 解决方案:
      • TTL 随机化: 如前述,在基础 TTL 上增加随机值。
      • 高可用架构: Redis Sentinel 或 Redis Cluster 避免单点故障。
      • 多级缓存: 结合本地缓存 (如 Caffeine) 和 Redis。本地缓存可扛住部分瞬时压力。
      • 服务降级 & 熔断: 使用 Hystrix, Resilience4j 等,在 DB 压力过大或不可用时,进行降级(返回兜底数据)或熔断(快速失败)。
      • 提前预热: 在预期流量高峰前,主动加载热点数据到缓存。

5. 缓存内容与设计

  • 缓存粒度:
    • 根据查询模式选择:缓存整个聚合对象、DTO 还是部分字段?
    • 细粒度(字段)节省内存,但可能导致多次查询组装。
    • 粗粒度(对象)方便,但更新时容易失效过多数据或包含冗余信息。平衡是关键。
  • 避免缓存大对象/集合:
    • 大对象消耗内存多,网络传输慢。
    • 考虑分页缓存或缓存 ID 列表,再按需查询详情(缓存关联)。
  • 缓存 VS 计算: 如果计算成本远低于一次网络IO+反序列化,则缓存可能得不偿失。进行基准测试。
  • 命名约定:cacheNames 使用清晰、一致的命名(如 user:profile, product:detail, order:summary)。

6. 监控、日志与运维

  • 监控指标:
    • Redis 自身: Memory Usage, Connected Clients, Command Latency, Hit/Miss Rate (需额外配置), Key Expirations, Evictions, CPU, Network。
    • 应用指标 (Micrometer): cache.gets, cache.puts, cache.removals, cache.hits, cache.misses, cache.lock.duration (使用 @Cacheable(sync=true) 时)。集成到 Prometheus + Grafana。
  • 慢查询日志: 配置 Redis 的 slowlog-log-slower-thanslowlog-max-len,定期分析。
  • 连接池监控: 监控连接池活跃连接、等待连接数等。
  • 日志:CacheManagerCache 级别适当添加日志(注意性能影响),记录关键操作(如缓存未命中、清除、大Key操作)。
  • 大 Key 扫描: 定期使用 redis-cli --bigkeys 或自定义脚本扫描,优化大 Key(拆分、压缩、改变数据结构)。
  • 内存分析: 使用 redis-rdb-tools 分析 RDB 文件,找出内存占用大户。

7. 高级主题/优化

  • 多级缓存:
    • 本地缓存 (Caffeine/Guava Cache) + Redis。
    • 本地缓存存放极热数据(TTL 很短,如秒级),Redis 存放次热数据。
    • 注意本地缓存一致性(可通过 Redis Pub/Sub 或 TTL 解决)。
  • 缓存预热:
    • 系统启动后或低峰期,主动加载预测的热点数据到缓存。
  • 非阻塞操作 (Reactive): 如果使用 Spring WebFlux,考虑使用 ReactiveRedisTemplate 和响应式的缓存抽象(较少见)。
  • Redis 数据结构选择: 除了简单的 String (K-V),考虑:
    • Hash (HSET/HGET): 缓存对象,可以部分更新字段(需结合 @CachePut 策略)。
    • Sorted Set (ZSET): 缓存排行榜等需要排序的数据。
    • Set (SET)/List (LIST): 特定场景(好友列表、最新N条)。
  • Pipeline/Multi-Get: 对于批量操作,使用 Redis Pipeline 或 MGET(需注意序列化)减少网络往返。

总结 Checklist:

  1. 使用 Lettuce 并配置连接池
  2. 弃用 JDK 序列化,配置合适的 JSON 序列化器 (Key: String, Value: JSON)。
  3. 为所有缓存设置合理的 TTL (全局默认 + 按需覆盖) 并考虑随机抖动
  4. 使用 @Cacheable(sync = true) 防御缓存击穿
  5. 实施缓存空值布隆过滤器防御缓存穿透
  6. 监控关键 Redis 指标和应用缓存指标 (Hit Rate, Latency)。
  7. 合理设计缓存粒度和内容。
  8. 定期扫描大 Key慢查询
  9. (可选) 考虑多级缓存 (本地 + Redis) 或缓存预热
  10. 清晰命名 cacheNames 和设计 key SpEL。

遵循这些实践能显著提升你的 Spring Cache + Redis 方案的健壮性和性能。务必根据你的具体应用场景(数据量、并发量、QPS、SLA)进行调整和测试。