一、前言:为什么需要 JVM 进程缓存?
你可能已经用 Redis 缓解了数据库压力,但在高并发场景下仍面临:
- ❌ Redis 网络延迟高(0.5~2ms),无法满足 <1ms 的响应要求
- ❌ 热点数据反复穿透到 Redis,造成不必要的网络开销
- ❌ 突发流量打垮缓存层
JVM 进程缓存(即本地缓存) 正是解决这些问题的“最后一公里”方案——将热点数据缓存在应用进程内存中,实现微秒级访问!
本文将带你彻底搞懂:
✅ 什么是 JVM 进程缓存?
✅ 主流框架如何选型?
✅ 如何避免内存泄漏与 OOM?
✅ 与 Redis 如何协同构建多级缓存?
二、什么是 JVM 进程缓存?
JVM 进程缓存(也称本地缓存)是指将数据直接存储在应用进程的堆内存中,无需跨网络调用,访问速度极快。
核心特点:
| 存储位置 | 应用 JVM 堆内存(如 ConcurrentHashMap) |
| 访问速度 | < 1 微秒(比 Redis 快 1000 倍以上) |
| 作用范围 | 单机有效(每个应用实例独立缓存) |
| 典型用途 | 缓存配置、字典表、热点商品、用户会话等 |
✅ 适用场景:读多写少、数据量小、变更不频繁的热点数据。
三、主流 JVM 本地缓存框架对比
3.1 手写 ConcurrentHashMap(不推荐)
private static final Map<String, Object> cache = new ConcurrentHashMap<>();
- ❌ 无自动过期
- ❌ 无容量控制
- ❌ 无淘汰策略 → 极易 OOM
3.2 Guava Cache(Google 出品)
LoadingCache<String, User> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<String, User>() {
public User load(String key) throws Exception {
return loadFromDB(key);
}
});
- ✅ 支持 LRU 淘汰
- ✅ 自动加载(get 时自动回源)
- ⚠️ 性能一般,已进入维护模式
3.3 Caffeine(强烈推荐!)
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats() // 开启统计
.build();
- ✅ 高性能:基于 Window TinyLFU 算法,命中率更高
- ✅ 低开销:异步清理、无锁设计
- ✅ Spring Boot 3.x 官方默认本地缓存
- ✅ 支持异步加载、权重淘汰、统计监控
📊 性能对比(100 万次 get 操作):
- ConcurrentHashMap:≈ 80ms(无淘汰)
- Guava Cache:≈ 120ms
- Caffeine:≈ 60ms(且内存占用更低)
四、Caffeine 核心特性详解
4.1 淘汰策略
- Size-based:maximumSize(1000) —— 最多缓存 1000 个条目
- Time-based:
- expireAfterWrite:写入后过期
- expireAfterAccess:最后访问后过期
- Weight-based:按对象大小淘汰
.maximumWeight(100_000)
.weigher((String key, User user) -> user.getAvatar().length())
4.2 自动加载(LoadingCache)
LoadingCache<Long, Product> productCache = Caffeine.newBuilder()
.build(id -> productMapper.selectById(id)); // 自动回源
Product p = productCache.get(1001L); // 若不存在,自动调用 mapper
4.3 异步操作
AsyncLoadingCache<Long, User> asyncCache = Caffeine.newBuilder()
.buildAsync(id -> CompletableFuture.supplyAsync(() -> loadUser(id)));
4.4 监控统计
Cache<String, Object> cache = Caffeine.newBuilder().recordStats().build();
CacheStats stats = cache.stats();
System.out.println("命中率: " + stats.hitRate()); // 如 0.95
五、JVM 缓存 vs 分布式缓存(Redis)
| 延迟 | < 1μs | 0.5~2ms |
| 吞吐 | 百万 QPS | 十万 QPS |
| 一致性 | 单机一致 | 集群一致 |
| 数据共享 | ❌ 不共享 | ✅ 全集群共享 |
| 持久化 | ❌ 进程重启丢失 | ✅ 可持久化 |
| 适用场景 | 热点、小数据 | 全量、大容量 |
💡 最佳实践:两者结合,构建多级缓存!
- 先查本地缓存(L1)
- 未命中再查 Redis(L2)
- 再未命中查 DB
六、避坑指南:如何避免 OOM 和脏数据?
❌ 坑 1:缓存无限增长 → OOM
- 解决方案:
- 必须设置 maximumSize
- 监控缓存大小(通过 cache.estimatedSize())
- 使用 weakKeys() / softValues()(谨慎使用)
❌ 坑 2:数据更新后缓存不一致
- 场景:用户修改昵称,但本地缓存仍为旧值
- 解决方案:
- 短 TTL:本地缓存 TTL << Redis TTL(如 5min vs 30min)
- 主动失效:更新 DB 时,清除本机缓存
public void updateUser(User user) {
userMapper.update(user);
localCache.invalidate(user.getId()); // 清除本机缓存
redisTemplate.delete("user:" + user.getId()); // 清除 Redis
} - 注意:无法清除其他机器的本地缓存 → 需接受短暂不一致
❌ 坑 3:缓存穿透(查询不存在的数据)
- 解决方案:
- 对 null 结果也缓存(TTL 较短,如 1min)
- 使用布隆过滤器预判
七、生产环境最佳实践
✅ 配置建议
@Bean
public Cache<String, Object> localCache() {
return Caffeine.newBuilder()
.maximumSize(10_000) // 控制内存
.expireAfterWrite(5, TimeUnit.MINUTES) // 短 TTL
.recordStats() // 开启监控
.executor(Runnable::run) // 使用当前线程(避免线程池开销)
.build();
}
✅ 监控指标(接入 Micrometer / Prometheus)
- 缓存命中率(目标 > 85%)
- 缓存大小(防止突增)
- 加载失败次数
✅ 适用数据类型
- ✅ 配置信息(如运营开关)
- ✅ 字典表(国家、城市列表)
- ✅ 热点商品/文章
- ❌ 用户私有数据(如购物车)→ 用 Redis 更安全
八、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!


