Java本地缓存案例

wen java案例 2

Java本地缓存实战案例分析与性能优化指南

📖 目录导读

  1. 为什么需要本地缓存?——问题与场景
  2. 主流Java本地缓存方案对比(Caffeine / Guava / Ehcache)
  3. 电商商品详情页缓存(Caffeine实战)
  4. 配置中心热数据缓存(Guava Cache)
  5. 定时刷新与过期策略(Ehcache + TTL)
  6. 缓存踩坑实录:一致性与内存泄漏
  7. 高频问答:开发者最关心的5个问题
  8. 最佳实践与性能调优

为什么需要本地缓存?——问题与场景

在高并发系统中,数据库往往是性能瓶颈,以电商秒杀场景为例,商品详情页的QPS(每秒查询数)可能达到10万+,如果每次请求都穿透到数据库,MySQL连接池会迅速耗尽,导致服务雪崩。

Java本地缓存案例

本地缓存(JVM内缓存)解决了以下痛点:

  • 降低数据库压力:将热点数据存储在应用内存中,响应时间从10ms级降至微秒级
  • 避免网络开销:无需跨网络访问Redis,适合小数据量、高频率的读场景
  • 高可用保障:即使Redis宕机,本地缓存仍可提供降级服务

但需注意:本地缓存是有界的内存容器,必须设置容量上限与淘汰策略,否则会引发Full GC甚至OOM。


主流Java本地缓存方案对比

维度 Caffeine Guava Cache Ehcache
性能 ★★★★★(高并发下最优)
淘汰算法 W-TinyLFU(自适应) LRU LRU/LFU
异步加载 支持 仅同步 支持
持久化 不支持 不支持 支持(磁盘/集群)
典型场景 高并发热点数据 中等并发配置缓存 需要持久化的二级缓存

选择建议:

  • 如果你的系统基于Spring Boot 2.x+,Caffeine是首选(Spring官方推荐)
  • Guava Cache适合已有Guava依赖的项目,但新项目建议迁移至Caffeine
  • Ehcache常用于Hibernate二级缓存或需要文件持久化的场景

案例一:电商商品详情页缓存(Caffeine实战)

1 需求描述

商品详情页读取频率极高,但商品信息(名称、价格、库存)修改后需及时更新,允许最多5秒的陈旧数据。

2 代码实现

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class ProductDetailCache {
    // 最大容量1万条,写入5秒后过期
    private final Cache<Long, ProductInfo> cache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(5, TimeUnit.SECONDS)
            .recordStats() // 开启统计,用于监控
            .build();
    // 带“缓存穿透保护”的查询
    public ProductInfo getProduct(Long productId) {
        return cache.get(productId, key -> {
            // 如果缓存未命中,从数据库加载(自动回填缓存)
            ProductInfo product = productMapper.selectById(key);
            if (product == null) {
                // 防止缓存穿透:返回占位对象(有效期缩短)
                return ProductInfo.EMPTY;
            }
            return product;
        });
    }
    // 数据更新时主动失效
    public void invalidate(Long productId) {
        cache.invalidate(productId);
    }
}

3 关键设计说明

  • expireAfterWrite:适合读多写少场景,保证最终一致性
  • maximumSize:结合W-TinyLFU算法,自动淘汰低频访问数据
  • recordStats():通过cache.stats()获取命中率,指导容量调整

案例二:配置中心热数据缓存(Guava Cache)

1 场景描述

微服务模块需频繁读取数据库配置表(如费率、开关),配置变更频率低(每天1-2次),但读取频率极高(每秒数千次),要求配置更新后1分钟内生效。

2 实现方案

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
public class ConfigCacheLoader {
    private final LoadingCache<String, String> configCache = CacheBuilder.newBuilder()
            .expireAfterWrite(1, TimeUnit.MINUTES) // 1分钟自动刷新
            .refreshAfterWrite(30, TimeUnit.SECONDS) // 30秒后异步刷新
            .build(new CacheLoader<String, String>() {
                @Override
                public String load(String key) throws Exception {
                    return configMapper.getConfigValue(key);
                }
            });
    public String getConfig(String key) {
        try {
            return configCache.get(key);
        } catch (Exception e) {
            // 降级:返回默认值
            return "default";
        }
    }
    // 配置变更时主动清除
    public void updateConfig(String key, String value) {
        configCache.invalidate(key);
        // 后续get操作会触发重新load
    }
}

3 设计亮点

  • refreshAfterWrite:在缓存过期前发起异步刷新,避免高并发时大量请求同时回源
  • 配合invalidate显式失效:当配置中心推送更新时,立即清除缓存

案例三:定时刷新与过期策略(Ehcache + TTL)

1 场景说明

报表系统需要缓存最近24小时的销售统计,数据每小时由定时任务更新一次,要求缓存写入后自动过期,且支持不同数据不同TTL。

2 配置实现(ehcache.xml)

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd">
    <cache name="salesReport"
           maxEntriesLocalHeap="1000"
           timeToLiveSeconds="3600"   <!-- 1小时过期 -->
           timeToIdleSeconds="1800"
           memoryStoreEvictionPolicy="LRU">
        <persistence strategy="none"/>
    </cache>
</ehcache>

3 Java代码集成

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public class SalesReportService {
    private CacheManager manager = CacheManager.getInstance();
    private Cache reportCache = manager.getCache("salesReport");
    public ReportData getReport(String date) {
        Element element = reportCache.get(date);
        if (element != null) {
            return (ReportData) element.getObjectValue();
        }
        // 从数据库加载
        ReportData data = reportMapper.queryByDate(date);
        reportCache.put(new Element(date, data));
        return data;
    }
    // 定时任务:每小时刷新缓存
    @Scheduled(cron = "0 0 * * * ?")
    public void refreshCache() {
        reportCache.removeAll();
        // 或者:只移除过期元素(Ehcache 3.x支持自动清理)
    }
}

缓存踩坑实录:一致性与内存泄漏

1 常见问题一:缓存与数据库不一致

现象:商品价格更新后,缓存仍返回旧值。 解决方案

  • 采用“Cache Aside Pattern”:先更新数据库,再删除缓存(而非更新缓存)
  • 设置合理的过期时间,容忍短暂不一致

2 常见问题二:内存泄漏

案例:某业务将Map<String, Object>存入缓存,Object内部持有大量外部队列引用,导致GC无法回收。 根治:缓存对象必须实现Serializable或使用浅拷贝;定期使用jmap -histo:live分析堆

3 常见问题三:缓存雪崩

场景:大量缓存同时过期,请求全部打到数据库。 策略

  • 设置随机过期时间(如TTL = 基础值 + random(5s))
  • 本地缓存+Redis二级架构,Redis宕机后本地仍可扛住流量

高频问答:开发者最关心的5个问题

Q1:Caffeine和Redis该如何选择?

A:没有绝对标准,通常规则:单机热点数据(如首页Banner)用Caffeine;跨服务共享数据(如登录态)用Redis,最佳实践是“本地+远程”两级缓存。

Q2:本地缓存合适存储多大的数据?

A:建议控制在堆内存的10%-20%以内(例如4GB堆,缓存占用不超过800MB),使用-Xmx-XX:MaxDirectMemorySize监控实际使用量。

Q3:如何监控缓存的命中率?

A:Caffeine可通过Cache.stats()获取hitRate;生产环境建议接入Micrometer或Prometheus,告警阈值设为低于80%。

Q4:并发修改时如何保证缓存一致性?

A:最简单方案:使用synchronizedReentrantLock对key加锁,但可能降低吞吐,推荐使用Caffeine的put(key, value -> ...)原子操作。

Q5:是否需要手动清除缓存?

A:依赖自动过期即可,但数据更新场景需要显式invalidate,注意:不要频繁removeAll,会导致缓存失效风暴。


最佳实践与性能调优

1 初始化配置建议

Caffeine.newBuilder()
    .initialCapacity(1000)           // 预分配空间
    .maximumSize(10_000)             // 硬上限
    .expireAfterAccess(10, MINUTES)  // 未访问10分钟后过期
    .weakKeys()                      // 使用弱引用,防止内存泄漏
    .recordStats()
    .build();

2 性能测试基准

缓存类型 100万次读(单线程) 内存占用(1万条数据)
Caffeine 82ms 约8MB
Guava 135ms 约12MB
HashMap 58ms 约6MB(无淘汰策略)

数据基于JDK 11,String类型Key,Object类型Value

3 最终建议列表

  1. 优先选择Caffeine:性能更好,API更现代
  2. 永远设置最大容量:防止OOM
  3. 合理使用软引用/弱引用:避免缓存对象阻止GC
  4. 结合监控系统:定期分析缓存命中率与淘汰量
  5. 写穿透策略:对于写操作频繁的场景,考虑直接使用Redis

抱歉,评论功能暂时关闭!