1. JCache与驱逐策略基础认知
JCache作为Java标准化的缓存API(JSR-107),其驱逐策略(Eviction Policy)是面试官最爱深挖的技术点之一。我在实际项目中使用JCache近五年,发现80%的性能问题都与驱逐策略配置不当有关。基于容量的驱逐(Capacity-based Eviction)作为最常用的策略类型,其核心逻辑是当缓存条目数达到阈值时,按照特定算法自动移除数据。
与时间过期策略不同,容量驱逐是空间维度的控制手段。举个例子:就像超市货架只能摆放100件商品,新货到仓时就必须下架旧商品。JCache规范要求所有实现(Ehcache、Hazelcast等)至少支持LRU(最近最少使用)算法,但实际生产环境中我们往往需要更精细的控制。
2. 配置容量驱逐的三种方式
2.1 声明式配置(推荐)
通过XML或YAML文件定义是最规范的做法。以下是Ehcache 3.x的典型配置:
<config xmlns="http://www.ehcache.org/v3"> <cache alias="orderCache"> <expiry> <ttl unit="minutes">30</ttl> </expiry> <resources> <heap unit="entries">1000</heap> <!-- 关键容量参数 --> <offheap unit="MB">100</offheap> </resources> <eviction-priority>LRU</eviction-priority> </cache> </config>关键参数解析:
<heap unit="entries">:设置基于条目数的容量上限<heap unit="MB">:改用内存大小作为阈值LFU/LRU:可选算法,LFU(最不常用)适合热点数据场景
2.2 编程式配置
动态创建缓存时更灵活,适合需要运行时调整的场景:
CachingProvider provider = Caching.getCachingProvider(); CacheManager manager = provider.getCacheManager(); MutableConfiguration<Long, Order> config = new MutableConfiguration<Long, Order>() .setTypes(Long.class, Order.class) .setExpiryPolicyFactory(FactoryBuilder.factoryOf( new AccessedExpiryPolicy(new Duration(TimeUnit.MINUTES, 30)))) .setStatisticsEnabled(true) .add(new MutableConfiguration.EntryListenerConfiguration<>( new OrderCacheListener(), null, false, true)) .setEvictionFilterFactory((Factory<EvictionFilter<Long, Order>>) () -> (key, value) -> !value.isLocked()) // 自定义过滤逻辑 .setResourcePools( ResourcePoolsBuilder.newResourcePoolsBuilder() .heap(1000, EntryUnit.ENTRIES) // 容量限制 .build());避坑指南:
- 务必调用
setStatisticsEnabled(true),否则无法监控驱逐效果 - 通过
EvictionFilter可以保护重要数据不被驱逐 - Hazelcast等实现可能需要额外依赖(如
hazelcast-jcache)
2.3 注解驱动配置
Spring Boot项目中可以结合@CacheConfig使用:
@Configuration @EnableCaching public class CacheConfig { @Bean public JCacheManagerCustomizer orderCacheCustomizer() { return cm -> { cm.createCache("orderCache", new MutableConfiguration<>() .setResourcePools( ResourcePoolsBuilder.newResourcePoolsBuilder() .heap(500)) .setExpiryPolicyFactory( AccessedExpiryPolicy.factoryOf(Duration.ONE_HOUR)) ); }; } }3. 高级调优策略
3.1 分层存储配置
当单层容量不足时,可采用堆外内存+磁盘的分层方案:
ResourcePoolsBuilder.newResourcePoolsBuilder() .heap(1000, EntryUnit.ENTRIES) // 第一层:堆内存 .offheap(1, MemoryUnit.GB) // 第二层:堆外内存 .disk(10, MemoryUnit.GB) // 第三层:磁盘 .build();性能对比数据:
| 存储层 | 读写延迟 | 适用场景 |
|---|---|---|
| 堆内存 | 100ns级 | 高频访问小对象 |
| 堆外内存 | 1μs级 | 大对象缓存 |
| 磁盘 | 10ms级 | 冷备数据 |
3.2 自定义驱逐算法
实现EvictionPolicy接口可覆盖默认行为:
public class WeightedEvictionPolicy implements EvictionPolicy<Long, Order> { @Override public boolean compare(EntryView<Long, Order> e1, EntryView<Long, Order> e2) { return e1.getValue().getWeight() > e2.getValue().getWeight(); } } // 注册自定义策略 cacheConfig.setEvictionPolicyFactory(FactoryBuilder.factoryOf( new WeightedEvictionPolicy()));3.3 监控与调优
通过JMX获取驱逐统计信息:
Cache<Long, Order> cache = cacheManager.getCache("orderCache"); long evictionCount = ((JCacheStatisticsMXBean) cache.unwrap(javax.cache.management.CacheMXBean.class)) .getCacheEvictions();健康检查指标:
- 驱逐率 = 驱逐次数 / 缓存命中数
- 当驱逐率 > 20%时建议扩容
- GC次数突增可能提示堆内存不足
4. 生产环境问题排查
4.1 典型故障案例
案例1:缓存穿透
- 现象:QPS 500+时出现周期性延迟飙升
- 根因:容量设置过小导致高频数据被误驱逐
- 解决方案:调整
heap(2000)并添加预热逻辑
案例2:算法选择不当
- 现象:热点数据频繁失效
- 根因:使用FIFO算法导致最近访问数据被移除
- 修复:改用
LRU并设置权重因子
4.2 参数优化公式
计算合理容量的经验公式:
理想容量 = (平均QPS × 最大响应时间) / (1 - 容忍穿透率)例如:
- QPS=300,平均RT=50ms,容忍10%穿透
- 容量 = (300×0.05)/(1-0.1) ≈ 1667
4.3 多级缓存策略
与Caffeine等本地缓存配合使用:
@Bean public CacheManager multiLevelCacheManager() { CaffeineCacheManager localCache = new CaffeineCacheManager(); localCache.setCaffeine(Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES)); JCacheCacheManager remoteCache = new JCacheCacheManager(); // JCache配置... return new ChainedCacheManager(localCache, remoteCache); }5. 最新技术动态
JCache 2.0草案中提出的改进:
- 弹性容量(根据负载动态调整)
- 基于成本的驱逐(综合考虑内存占用和计算开销)
- 机器学习驱动的智能淘汰算法
对于高频面试题"如何选择LRU与LFU",我的经验是:LRU适合时间局部性强的场景(如用户会话),LFU更适合长期热点(如商品目录)。实际项目中往往需要组合使用,比如Hazelcast的"LRU+时间衰减"混合策略。