bigywater.tech
Spring · 101

Spring Cache + Redis:穿透、击穿、雪崩的配置位置

@Cacheable 加 Redis 之后,穿透、击穿、雪崩各要在哪里配置?cache-null-values 的默认值是什么?sync = true 真的能防击穿吗?TTL 怎么加随机?这篇在 Spring Boot 4.0.8 加 Redis 上逐个实测。

30 次 vs 1 次
sync = true 时 30 个线程请求同一个冷 key:默认写入器执行 30 次,lockingRedisCacheWriter 执行 1 次
TTL = -1
没有设置 time-to-live 的 Redis 缓存永不过期
1 种 vs 10 种
100 个 key 统一 TTL 20 秒只有 1 种剩余 TTL;TtlFunction 随机后有 10 种

key 与 TTL

实测 · 本机执行输出
  byId(1) 两次、byId(2) 一次:方法体真正执行了 2 次;CacheManager = RedisCacheManager
  redis 里的 key b'users::1':TTL = -1(-1 表示永不过期),value 13 字节
  redis 里的 key b'users::2':TTL = -1(-1 表示永不过期),value 13 字节
  byId(1) 两次、byId(2) 一次:方法体真正执行了 2 次;CacheManager = RedisCacheManager
  加 spring.cache.redis.time-to-live=60s 之后:[(b'users::1', 60), (b'users::2', 60)]
  byId(1) 两次、byId(2) 一次:方法体真正执行了 2 次;CacheManager = RedisCacheManager
  加 spring.cache.redis.use-key-prefix=false 之后 key:[b'1', b'2']

key 是「缓存名::key」,默认永不过期;time-to-live 是全局设置。

穿透

实测 · 本机执行输出
  nullable(999) 调两次(方法体永远返回 null):方法体执行 1 次
  nullableUnless(999) 调两次(方法体永远返回 null):方法体执行 2 次
  redis 里留下的 key:[(b'users::999', 60, 64)](第一个是缓存了 null 的 users::999)
  cache-null-values=false:nullable(999) 调两次(方法体永远返回 null):方法体执行 1 次;异常 IllegalArgumentException:Cache 'users' does not allow 'null' values; Avoid storing null via '@Cacheable(unless="#result == null")' or configure RedisCache to allow 'null' via RedisCacheConfiguration
  cache-null-values=false:nullableUnless(999) 调两次(方法体永远返回 null):方法体执行 2 次

cache-null-values 默认 true,null 也被缓存;unless 可以不缓存 null;关掉后遇到 null 会抛异常。

击穿

实测 · 本机执行输出
  sync = false(默认):30 个线程同时请求同一个冷 key,方法体执行 30 次,总用时 582 ms
  sync = true:30 个线程同时请求同一个冷 key,方法体执行 30 次,总用时 569 ms
  sync = true,写入器换成 lockingRedisCacheWriter:30 个线程同时请求同一个冷 key,方法体执行 1 次,总用时 995 ms
  -- 两个进程(模拟两个实例)同时请求同一个冷 key,sync = true + lockingRedisCacheWriter:
    sync = true,写入器换成 lockingRedisCacheWriter:30 个线程同时请求同一个冷 key,方法体执行 1 次,总用时 1141 ms
    sync = true,写入器换成 lockingRedisCacheWriter:30 个线程同时请求同一个冷 key,方法体执行 0 次,总用时 1191 ms
    两个进程的方法体执行次数相加 = 1

sync = true 要配合 lockingRedisCacheWriter 才有效,代价是等锁;锁在 Redis 里所以跨实例也有效。

雪崩

实测 · 本机执行输出
  写入完成:users 100 个、jitter 100 个
  users(全局统一 TTL 20 秒):100 个 key 的剩余 TTL 取值 [20]
  jitter(entryTtl 用 TtlFunction,10~19 秒随机):100 个 key 的剩余 TTL 最小 10、最大 19,不同的取值有 10 个

统一 TTL 会同时过期;用 TtlFunction 给 TTL 加随机。

分步演示

缓存
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动(Spring Data Redis 4.0.7 的 RedisCacheManager;Redis 是本机自己起的实例,端口 7391,不碰用户自己的 6379)。每个场景一个独立 JVM(非 Web);「两个实例」是两个进程同时跑;TTL 与 key 取自 Redis 的 KEYS / TTL;击穿实验的方法体里 sleep 300 ms,30 个线程同时开始
    • 没有验证:lockingRedisCacheWriter 的等锁间隔参数与锁泄漏(进程死亡时锁怎么办);Caffeine 本地缓存与 Redis 组成多级缓存;缓存与数据库的一致性(先更新数据库还是先删缓存);@CachePut / @CacheEvict 的行为;热点 key 在 Redis 集群中的分布;布隆过滤器防穿透;缓存预热
    京ICP备2026067502号-1京公网安备11010502063487号