欢迎光临
我们一直在努力

JVM进程缓存

一、前言:为什么需要 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)

对比项JVM 本地缓存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 更安全

八、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

赞(0)
未经允许不得转载:171主机测评 » JVM进程缓存
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址