缓存的基本读写
最常见的「Cache Aside」模式:
- 读:先查缓存,命中直接返回;未命中查库,写入缓存。
- 写:先更新数据库,再删除缓存(而非更新缓存)。
读: cache.get(k) → 命中返回; 未命中 db.query → cache.set(k, v, ttl)
写: db.update → cache.del(k)
缓存穿透:查根本不存在的数据
成因:请求的参数在数据库中不存在(如 id = -1),缓存和数据库都查不到,恶意流量直接打到 DB。
解法一:缓存空值
v = cache.get(k)
if v == nil:
v = db.query(k)
if v == nil:
cache.set(k, NULL, 60s) # 缓存短时间空标记
return v
解法二:布隆过滤器——在缓存前加一层 BloomFilter,不存在的 key 直接拒绝。
if !bloom.mightContain(k): return nil # 一定不存在
缓存击穿:热点 key 突然失效
成因:某个超热点 key(如首页配置)过期瞬间,海量请求同时穿透到 DB。
解法:互斥锁(singleflight)
v = cache.get(k)
if v == nil:
if lock.tryAcquire(k): # 只有一个线程回源
v = db.query(k)
cache.set(k, v, ttl)
lock.release(k)
else:
sleep(50ms); retry() # 其余线程等待并重试
另一种思路是逻辑过期:value 里带过期时间戳,异步刷新,读时不阻塞。
缓存雪崩:大量 key 同时失效
成因:批量 key 设置了相同 TTL,同一时刻集体失效,DB 瞬间被打垮。
解法:随机过期时间
ttl = base + random(0, jitter) # 例如 3600 ± 600 秒
cache.set(k, v, ttl)
配合多级缓存(本地 Caffeine + Redis)与限流降级,可进一步兜底。
最佳实践小结
- 给所有 key 设 TTL,避免无限堆积;
- 写操作删缓存而非更新缓存;
- 大 value 拆分,避免热 key 集中;
- 监控命中率,命中率低于 90% 要警惕。
缓存的本质是用空间换时间,代价是一致性。明确业务能接受的最终一致窗口,再决定策略。