实现JVM进程缓存

一、前言:为什么"手写缓存"会翻车?

很多开发者在项目初期为了"简单",直接用 ConcurrentHashMap 实现本地缓存:

java 复制代码
private static final Map<String, Object> cache = new ConcurrentHashMap<>();

看似没问题,但上线后却遭遇:

  • 内存持续增长,最终 OOM
  • 热点数据无法自动刷新,导致脏读
  • 缓存击穿打垮数据库

真正的 JVM 进程缓存,必须具备:容量控制 + 自动过期 + 淘汰策略

本文将带你从错误示范 → 正确实现 → 生产级方案,彻底掌握 JVM 进程缓存的实现之道。


二、错误示范:手写缓存的三大致命缺陷

缺陷 1️⃣:无限增长 → OOM

java 复制代码
// 危险!无上限缓存
Map<Long, User> userCache = new ConcurrentHashMap<>();
userCache.put(userId, user); // 永远不删,内存迟早爆

缺陷 2️⃣:无过期机制 → 脏数据

java 复制代码
// 用户修改了昵称,但缓存还是旧的
String name = userCache.get(1001L); // 返回 "张三"(实际已改名)

缺陷 3️⃣:无并发控制 → 缓存击穿

java 复制代码
public User getUser(Long id) {
    User user = cache.get(id);
    if (user == null) {
        user = loadFromDB(id); // 多个线程同时查 DB!
        cache.put(id, user);
    }
    return user;
}

💥 结果:高并发下,数据库被瞬间打垮!


三、正确姿势 1:基于 Guava Cache(过渡方案)

Google 的 Guava 提供了基础缓存能力:

java 复制代码
LoadingCache<String, User> cache = CacheBuilder.newBuilder()
    .maximumSize(10_000)                // 最大 1 万个条目
    .expireAfterWrite(10, TimeUnit.MINUTES) // 写入 10 分钟后过期
    .build(new CacheLoader<String, User>() {
        @Override
        public User load(String key) throws Exception {
            return userMapper.selectById(Long.parseLong(key));
        }
    });

// 使用:自动回源,线程安全
User user = cache.get("1001");

优点 :自动加载、线程安全、支持过期

⚠️ 缺点:性能一般,已停止活跃维护


四、正确姿势 2:使用 Caffeine(推荐!)

Caffeine 是目前 Java 本地缓存的黄金标准,由 Guava 原班人马打造,性能更强、功能更全。

4.1 基础实现:手动存取

java 复制代码
Cache<String, User> userCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(5, TimeUnit.MINUTES)
    .recordStats() // 开启统计
    .build();

// 存
userCache.put("1001", user);

// 取
User user = userCache.getIfPresent("1001");

4.2 高级实现:自动加载(推荐!)

java 复制代码
LoadingCache<Long, User> userCache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(userId -> {
        log.info("缓存未命中,从 DB 加载用户: {}", userId);
        return userMapper.selectById(userId); // 自动回源
    });

// 一行代码搞定缓存+DB 查询
User user = userCache.get(1001L);

优势

  • 线程安全:多线程并发 get 不会重复查 DB
  • 高性能:TinyLFU 算法,命中率更高
  • 低开销:异步清理,无锁设计

五、生产级实现:Spring Boot 集成 Caffeine

5.1 添加依赖

XML 复制代码
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

5.2 配置缓存 Bean

java 复制代码
@Configuration
public class LocalCacheConfig {

    @Bean("userCache")
    public LoadingCache<Long, User> userCache(UserMapper userMapper) {
        return Caffeine.newBuilder()
            .maximumSize(50_000)
            .expireAfterWrite(15, TimeUnit.MINUTES)
            .recordStats()
            .build(userId -> userMapper.selectById(userId));
    }
}

5.3 在 Service 中使用

java 复制代码
@Service
public class UserService {

    @Autowired
    @Qualifier("userCache")
    private LoadingCache<Long, User> userCache;

    public User getUser(Long id) {
        try {
            return userCache.get(id);
        } catch (Exception e) {
            log.error("加载用户缓存失败", e);
            throw new RuntimeException("获取用户失败");
        }
    }

    // 更新时清除缓存
    public void updateUser(User user) {
        userMapper.update(user);
        userCache.invalidate(user.getId()); // 主动失效
    }
}

六、关键避坑指南

🔒 坑 1:缓存雪崩(大量 key 同时过期)

  • 现象:Redis 或 DB 瞬间被打垮

  • 解决方案 :TTL 加随机值

    java 复制代码
    .expireAfterWrite(10 + new Random().nextInt(5), TimeUnit.MINUTES)

🕳️ 坑 2:缓存穿透(查不存在的数据)

  • 现象:恶意请求反复打 DB
  • 解决方案
    • 对 null 结果也缓存(TTL 短,如 1 分钟)
    • 使用布隆过滤器预判

🔄 坑 3:数据不一致

  • 现象:更新 DB 后,本地缓存仍是旧值
  • 解决方案
    • 短 TTL:本地缓存 TTL << Redis TTL
    • 主动失效 :更新时调用 invalidate(key)
    • 接受短暂不一致(本地缓存天然局限)

💥 坑 4:内存溢出(OOM)

  • 预防措施
    • 必须设置 maximumSize
    • 监控 cache.estimatedSize()
    • 避免缓存大对象(如图片、长文本)

七、性能对比:手写 vs Caffeine

方案 10 万次 get 耗时 内存占用 是否防击穿 是否自动过期
ConcurrentHashMap 80ms 持续增长
Guava Cache 120ms 稳定
Caffeine 60ms 更低

📊 结论:Caffeine 不仅更快,还更省内存!


八、监控与运维

8.1 查看缓存统计

java 复制代码
CacheStats stats = userCache.stats();
log.info("命中率: {}, 加载次数: {}", 
    String.format("%.2f%%", stats.hitRate() * 100),
    stats.loadCount());

8.2 接入 Micrometer(Spring Boot)

java 复制代码
@Bean
public CacheMetricsRegistrar caffeineCacheMetrics() {
    return new CaffeineCacheMetricsRegistrar();
}

然后通过 /actuator/metrics 查看 cache.gets, cache.puts 等指标。


九、结语

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

相关推荐
梅孔立18 小时前
两种免费的翻译API调用方式详解 - Edge官方API与个人缓存服务
前端·缓存·edge
gwf21618 小时前
SSD读写速度深度解析:顺序读写vs随机读写、IOPS、延迟,你的硬盘性能到底怎么看?
git·嵌入式硬件·缓存·github·智能硬件
番茄炒鸡蛋加糖21 小时前
缓存三大问题
缓存
磁爆步兵1 天前
内存分区:程序运行的核心秘密
java·开发语言·jvm
江晓鱼未暖1 天前
十七、Redis 核心原理与架构详解
大数据·数据库·数据仓库·redis·缓存·架构
小罗水1 天前
第13章 Redis 缓存、幂等锁与任务状态
数据库·redis·缓存
热心市民lcj1 天前
Spring Boot 整合 Caffeine 本地缓存实战
spring boot·后端·缓存
讓丄帝愛伱1 天前
JVM线上故障快速排
jvm
落苜蓿蓝1 天前
Java 循环中对象复用导致属性覆盖?从 JVM 内存模型讲解原因
java·jvm·python
一只专注api接口开发的技术猿1 天前
OpenClaw 实战:搭建京东商品监控与简易数据分析脚本
大数据·jvm·数据库·oracle·数据分析