cms项目细节-性能优化之缓存篇
什么是缓存
缓存就是把“算过的结果”存起来,下次直接用,不再重新算。最常见的缓存目标就是数据库查询结果:
第一次请求:方法执行 → 查数据库 → 返回结果
第二次请求:直接读内存 → 不查数据库 → 返回同样的结果项目里 MethodCacheAspect 做的事情就是这样,只是它是通过 AOP 自动做的:
@MethodCache(key = "site", ttlSeconds = 60)
public Site site(String acode) { ... }不是所有方法都能缓存,必须满足三个条件:
- 相同参数得到相同结果(确定性):
site("cn")每次返回的内容基本一样。 - 数据变化慢:站点标题、导航栏目不是每秒变化。
- 读多写少:所有访客都读,只有管理员偶尔写。
如果数据每秒都在变(比如余额、库存),就不能随便缓存。
详见cms项目细节-aop 可看本项目中缓存的作用
给热点方法加了缓存切面:命中缓存直接返回,不查数据库;
TTL 自动过期兜底,写操作主动清缓存。实测第二次请求 SQL 查询从 1 次降到 0 次。
单机内存缓存不够
ConcurrentHashMap 只在当前 JVM 内有效:
- 多实例部署时各自缓存,数据可能不一致。
- 服务重启缓存全丢。
- 生产用 Redis 共享缓存 + TTL,或用 Caffeine 做本地 + Redis 两级缓存。
缓存穿透、击穿、雪崩
缓存穿透
查询一个不存在的 key,缓存里永远没有,每次都查数据库。
例子:攻击者遍历不存在的 id,比如 id=-1、id=999999:
请求 id=999999 → 缓存没有 → 数据库也没有 → 不写缓存
请求 id=999998 → 缓存没有 → 数据库也没有 → 不写缓存
... 每个不存在的 id 都打到数据库为什么危险:数据库被无效查询打满,正常用户也被拖慢。
解决方案 1:空值缓存
数据库返回 null 时,也把空值缓存一段时间:
Object value = pjp.proceed();
if (value == null) {
cache.put(key, new CacheEntry(now, EMPTY)); // 空值也缓存 60 秒
} else {
cache.put(key, new CacheEntry(now, value));
}
return value;这样同一个不存在的 id 第二次来,缓存里能查到空值,不再打数据库。
解决方案 2:参数校验 + 布隆过滤器
- 参数明显非法(负数、超长)直接拒绝。
- id 大海捞针时用布隆过滤器判断“这个 key 可能不存在”,不存在直接返回。
缓存击穿
某一个热点 key 的缓存刚好过期,一瞬间大量请求同时来,全部打到数据库。
为什么和穿透不同:穿透是查不存在的 key;击穿是同一个存在但刚过期的热点 key。
解决方案:互斥锁(Mutex)
只有第一个请求去查库,其他请求等它查完直接读缓存:
Object value = cache.get(key);
if (value != null) return value;
synchronized (lockObject) { // 先加锁,只允许一个人进来
value = cache.get(key); // 双重检查:前面等待的人可能已经填好了
if (value != null) return value;
value = pjp.proceed(); // 只有第一个人真的查库
cache.put(key, new CacheEntry(now, value));
return value;
}关键细节是双重检查:第一个请求查完回填缓存后,其他等在锁外面的请求进来时直接命中缓存,不用再查库。
生产环境用 Redis 实现分布式锁:SETNX 成功的人才去查库,其他人短暂等待后读缓存。
缓存雪崩
大量 key 同时过期,导致一瞬间大量请求同时打到数据库。
和击穿的区别:击穿是一个热点 key;雪崩是一大批 key。
解决方案:
- 过期时间加随机值,让过期时间错开:
long ttl = methodCache.ttlSeconds() * 1000L;
long randomJitter = ThreadLocalRandom.current().nextLong(0, 5000); // 0~5秒随机
long expireAt = now + ttl + randomJitter;- 多级缓存:本地 Caffeine + Redis,Redis 挂了本地还能扛。
- 热点数据永不过期 + 后台异步更新:缓存不设 TTL,由任务或写操作刷新。
| 问题 | 场景 | 核心解法 |
|---|---|---|
| 穿透 | 查不存在的 key | 空值缓存、参数校验、布隆过滤器 |
| 击穿 | 一个热点 key 过期 | 互斥锁 + 双重检查 |
| 雪崩 | 大量 key 同时过期 | TTL 加随机值、多级缓存、永不过期+异步刷新 |
“缓存最怕三个问题:穿透是查不存在的 key,空值也缓存;击穿是单个热点 key 过期,用互斥锁只让一个线程查库;雪崩是大量 key 同时过期,TTL 加随机值错开。项目里目前是基础版缓存,这些是生产级演进方向。”