cache-traplisted
Install: claude install-skill 1398281322-a11y/java-backend-guardrails
# 缓存陷阱
## When to Invoke
商品详情、活动页、配置、用户信息等读多写少走 Redis;出现击穿/穿透/雪崩/不一致。
## When NOT
分布式锁、库存扣减主账本、MQ。缓存不是数据源。
## 风险(面试考点)
| 名称 | 是什么 | 不是什么 |
|------|--------|----------|
| 穿透 | 查不存在的数据,缓存和 DB 都没有,每次打 DB | 热点 key 过期 |
| 击穿 | 一个热点 key 过期,并发打穿 DB | 大批 key 一起过期 |
| 雪崩 | 大批 key 同时过期,或 Redis 宕机,流量打到 DB | 单个 key |
另:先更新缓存再写 DB、或删缓存后再写 DB 导致把旧值填回缓存。
## 适用场景
- 商品/景点/场次详情
- 活动开关、字典
- 热点商户信息
## 方案选型(轻量优先)
读写:Cache-Aside。应用读 Redis → miss 读 DB → 回填。写:**先写 DB,再删缓存**(不是改缓存)。删失败靠 TTL 自愈,可延迟再删一次。
| 问题 | 默认 | 升级 |
|------|------|------|
| 穿透 | 参数校验 + 缓存空值短 TTL(30–120s) | 布隆过滤器挡明确不存在的 ID |
| 击穿 | 热点永不过期或逻辑过期 + 单飞(同 key 只回源一次) | 互斥锁回填 |
| 雪崩 | TTL 加随机抖动;Redis 主从/哨兵 | 本地 Caffeine 做二级;DB 限流;熔断 |
| 热点 | 本地缓存 + Redis | 拆 key、副本读 |
| 大 key | 拆分;删除用 `UNLINK` | 禁止 `KEYS` |
## 默认方案
```java
public Product get(Long id) {
String key = "prod:" + id;
String cached = redis.opsForValue().get(key);
if (cached != null) {
return cached.equals(NULL) ? null : readJson(cached);
}
Product db = productMapper.selectById(id);
if (db == null) {
redis.opsForValue().set(key, NULL, Duration.ofSeconds(60));
return null;
}
int ttl = 600 + ThreadLocalRandom.current().nextInt(120);
redis.opsForValue().set(key, writeJson(db), Duration.ofSeconds(ttl));
return db;
}
@Transactional
public void update(Product p) {
productMapper.updateById(p);
redis.delete("prod:" + p.getId()); // 先 DB 后删缓存
}
```
击穿单飞(同进程):
```java
private final ConcurrentHashMap<L