Spring Cache + Redis 最佳实践指南,旨在帮助你构建高性能、稳定且可维护的缓存方案:
核心原则:
- 缓存是优化,不是数据源: 始终确保数据库是数据的唯一真实来源。缓存可能失效、被清除或包含过期数据。
- 明确缓存目的: 清晰定义缓存什么(热点数据、复杂计算结果)、为什么缓存(减轻DB负载、加速响应)以及缓存的粒度(对象、DTO、部分对象)。
- 理解 Cache Abstraction: Spring Cache 是抽象层,提供统一的编程模型(
@Cacheable,@CachePut,@CacheEvict)。具体实现(如 Redis)通过CacheManager和Cache接口接入。
最佳实践详解:
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。
- 必须配置: Lettuce 默认使用无界连接池,生产环境必须配置连接池限制 (
-
序列化器 (Critical!):
- 避免 JDK 序列化 (
JdkSerializationRedisSerializer): 慢、体积大、不安全、语言绑定强。 - 首选 JSON:
GenericJackson2JsonRedisSerializer: 最常用。存储类名信息,反序列化方便。注意处理复杂类型(如 LocalDateTime)和循环引用。Jackson2JsonRedisSerializer: 需要显式指定目标类型 (new Jackson2JsonRedisSerializer<>(MyClass.class))。不存储类名,更节省空间,但类型信息需应用层管理。
- 其他可选: Protobuf, Kryo (需自定义配置,性能好但兼容性/可读性稍差)。
- Value 序列化器: 为
RedisTemplate和RedisCacheManager显式设置合适的 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(); } - 避免 JDK 序列化 (
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): 使用
@Cacheable的unless条件或 AOP 拦截,将查询结果为null或特定“空对象”也缓存起来(设置一个较短的 TTL,如 2-5 分钟)。 - 布隆过滤器 (Bloom Filter): 在查询缓存/DB 前,先用内存中的布隆过滤器判断 key 可能不存在(快速失败)。适用于 key 空间有限且可预知的场景。Redis 4.0+ 支持
BF.ADD/BF.EXISTS模块。
- 缓存空值 (Null Object): 使用
- 缓存击穿 (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-than和slowlog-max-len,定期分析。 - 连接池监控: 监控连接池活跃连接、等待连接数等。
- 日志: 在
CacheManager或Cache级别适当添加日志(注意性能影响),记录关键操作(如缓存未命中、清除、大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条)。
- Hash (
- Pipeline/Multi-Get: 对于批量操作,使用 Redis Pipeline 或
MGET(需注意序列化)减少网络往返。
总结 Checklist:
- 使用 Lettuce 并配置连接池。
- 弃用 JDK 序列化,配置合适的 JSON 序列化器 (Key: String, Value: JSON)。
- 为所有缓存设置合理的 TTL (全局默认 + 按需覆盖) 并考虑随机抖动。
- 使用
@Cacheable(sync = true)防御缓存击穿。 - 实施缓存空值或布隆过滤器防御缓存穿透。
- 监控关键 Redis 指标和应用缓存指标 (Hit Rate, Latency)。
- 合理设计缓存粒度和内容。
- 定期扫描大 Key 和慢查询。
- (可选) 考虑多级缓存 (本地 + Redis) 或缓存预热。
- 清晰命名
cacheNames和设计keySpEL。
遵循这些实践能显著提升你的 Spring Cache + Redis 方案的健壮性和性能。务必根据你的具体应用场景(数据量、并发量、QPS、SLA)进行调整和测试。