实现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 等指标。


九、结语

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

相关推荐
oem1102 小时前
Django全栈开发入门:构建一个博客系统
jvm·数据库·python
njidf11 小时前
用Python制作一个文字冒险游戏
jvm·数据库·python
2403_8355684712 小时前
自然语言处理(NLP)入门:使用NLTK和Spacy
jvm·数据库·python
2301_7765087214 小时前
用Python生成艺术:分形与算法绘图
jvm·数据库·python
左左右右左右摇晃14 小时前
JVM 笔记--分代工程以及分代的算法
jvm·笔记
Arva .15 小时前
Spring 的三级缓存,两级够吗
java·spring·缓存
2401_8845632416 小时前
Python Lambda(匿名函数):简洁之道
jvm·数据库·python
庞轩px16 小时前
MinorGC的完整流程与复制算法深度解析
java·jvm·算法·性能优化
haixingtianxinghai16 小时前
Redis真的是单线程吗?
数据库·redis·缓存