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


九、结语

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

相关推荐
用户094248568033 小时前
第23章:OpenJDK逃逸分析、标量替换与锁优化
java·jvm
wuminyu3 小时前
LockStack在虚拟线程Mount和Unmount拷贝过程剖析
java·linux·c语言·jvm·c++
樱花落木兰3 小时前
分布式登录实战:Session 会话共享改造,Redis 存储用户登录状态
java·javascript·数据库·redis·分布式·缓存
ShineWinsu4 小时前
对于Redis:Steam、Geospatial、Hyperloglog、Bitmap、Bitfield类型的解析
数据库·c++·redis·分布式·缓存·面试·zset
imDwAaY6 小时前
Redis 也能做消息队列?从 Stream 的存储讲到消费确认
数据库·redis·缓存
2601_962071577 小时前
类变量和全局变量的隔离性有什么区别?
java·开发语言·jvm
林伽一7 小时前
决策模型接口趋同、缓存按字节计价,AI 技术栈的两处底层改写| 2026年10月04日
人工智能·缓存
海绵宝宝转agent7 小时前
LeetCode100 LRU缓存思路讲解
java·开发语言·缓存
估值探索者9 小时前
【Python量化系统工程实战 #01】数据存储选型 CSVSQLiteMySQL 对比与 SQLite 实战建库
开发语言·jvm·python·sqlite·api接口·数据api接口·股票数据api接口
打工仔折腾 AI21 小时前
从 Demo 到生产级 Agent:8 个关键设计机制与 Python 实现拆解
java·jvm·人工智能·后端·python·langchain·ai agent 实战