4. 缓存策略:本地缓存、远程缓存与命中率优化
缓存这东西,说白了就是用空间换时间。我见过太多团队一上来就上 Redis,结果发现大部分热点数据根本不需要跨进程共享。今天咱们把缓存策略拆开揉碎了讲,从本地到远程,再到命中率和失效策略,一条龙说清楚。
4.1 本地缓存:最快的路
本地缓存就是把数据放在进程内。没有网络开销,没有序列化成本,速度是纳秒级的。我个人习惯在以下场景优先考虑本地缓存:
- 配置信息:应用启动时加载,运行中几乎不变
- 字典数据:国家代码、状态枚举、权限列表
- 热点只读数据:首页推荐、热门标签
常用的实现方案有 Guava Cache、Caffeine、Ehcache。我推荐 Caffeine,性能比 Guava 高一个量级。来看个例子:
// Caffeine 本地缓存示例
Cache<String, UserProfile> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build();
// 查询时自动加载
UserProfile profile = cache.get("user_1001", key -> {
return userService.loadFromDB(key);
});
4.2 远程缓存:NFS 与 S3
本地缓存搞不定跨进程共享。比如多个实例需要访问同一份构建产物,或者共享一个大的模型文件。这时候就得用远程缓存。
4.2.1 NFS 缓存
NFS 适合小规模集群,延迟在毫秒级。我建议把它当作「共享磁盘」用,不适合高并发场景。曾经有个项目把 NFS 当 Redis 用,结果 IO 打满,整个构建集群都卡住了。
# 挂载 NFS 共享目录
mount -t nfs -o vers=4,hard,timeo=600,retrans=2 \
192.168.1.100:/data/cache /mnt/build-cache
4.2.2 S3 缓存
S3 适合大规模、跨地域的缓存场景。延迟虽然比 NFS 高(几十毫秒),但吞吐量和可靠性强得多。我一般在 CI/CD 流水线里用它存构建产物:
# 使用 aws-cli 上传缓存
aws s3 cp ./dist/ s3://my-build-cache/v1.2.3/ \
--recursive \
--cache-control "public, max-age=31536000"
你想想看,S3 的另一个好处是自带版本管理。万一缓存写坏了,回滚也方便。
4.3 缓存命中率优化
命中率是缓存的核心指标。低于 80% 的缓存基本是浪费资源。我总结了几条实战经验:
- 热点数据优先:用 LRU 或 LFU 淘汰策略,别用 FIFO
- 缓存粒度要细:别把整个页面缓存,拆成组件级别
- 预热机制:系统启动时主动加载热点数据
- 避免缓存穿透:对不存在的数据也缓存空值,设置短过期时间
命中率计算公式:
命中率 = 缓存命中次数 / (缓存命中次数 + 缓存未命中次数) × 100%
建议用监控工具(如 Prometheus + Grafana)实时跟踪这个指标。
我曾经接手过一个项目,缓存命中率只有 30%。查了半天发现是缓存 key 设计有问题——每次请求都带随机参数,导致缓存永远命中不了。改成分段 key 后,命中率直接飙到 92%。
4.4 缓存失效与清理策略
缓存失效比缓存写入难十倍。我见过太多因为缓存没及时清理导致的线上事故。常见的策略有:
| 策略 | 原理 | 适用场景 | 风险 |
|---|---|---|---|
| TTL 过期 | 写入时设置存活时间 | 数据变化不频繁 | 过期瞬间可能有雪崩 |
| 主动失效 | 数据更新时主动删除缓存 | 强一致性要求 | 实现复杂,容易漏删 |
| 版本号 | 缓存 key 带版本号 | 构建产物、静态资源 | 版本管理成本高 |
| 写穿透 | 更新数据库同时更新缓存 | 读写比例接近 | 并发写可能脏数据 |
4.4.1 缓存雪崩与击穿
这两个问题很常见,但很多人分不清:
- 缓存雪崩:大量缓存同时过期,请求全部打到数据库
- 缓存击穿:一个热点 key 过期,高并发请求瞬间压垮数据库
解决方案其实不复杂:
// 缓存雪崩:过期时间加随机偏移
int baseTTL = 300;
int randomOffset = new Random().nextInt(60);
int finalTTL = baseTTL + randomOffset;
// 缓存击穿:互斥锁
String key = "hot_data";
String value = cache.get(key);
if (value == null) {
if (redis.setnx("lock:" + key, "1", 10)) {
value = db.query(key);
cache.set(key, value, 300);
redis.del("lock:" + key);
} else {
Thread.sleep(50);
return cache.get(key); // 重试
}
}
4.5 缓存策略全景图
下面这张图总结了本章的核心知识体系,从缓存层级到策略选择,一目了然:
4.6 实战建议
最后给几条实在的建议:
- 先本地,后远程:能放本地的别放远程,减少网络开销
- 监控先行:上线前就把命中率、缓存大小、淘汰次数都监控起来
- 降级方案:缓存挂了要有兜底,别让用户看到 500
- 定期清理:远程缓存(尤其是 S3)要设生命周期策略,避免存储成本失控