1. 为什么需要本地缓存?
在Web应用开发中,数据库查询往往是性能瓶颈的主要来源。我经历过一个电商项目,商品详情页的QPS在促销时能达到5000+,如果每次请求都直接查询数据库,MySQL实例的CPU直接飙到100%。后来我们引入本地缓存后,数据库负载下降了70%以上。
本地缓存相较于分布式缓存(如Redis)有几个独特优势:
- 零网络开销:数据直接在应用进程内存中存取
- 极致速度:内存访问速度是网络请求的1000倍以上
- 简单可靠:不依赖外部服务,系统架构更简单
Ehcache作为老牌Java缓存框架,有着超过15年的发展历史。最新版的Ehcache 3.x完全兼容JSR-107标准,支持:
- 堆内/堆外/磁盘三级存储
- 灵活的过期策略
- 缓存数据持久化
- 缓存事件监听
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 项目环境搭建
2.1 依赖配置
在Spring Boot项目中引入Ehcache非常简单。以Maven项目为例,需要在pom.xml中添加:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.9.7</version>
</dependency>
注意:这里同时引入了JCache API和Ehcache实现,这是为了保持缓存使用的标准化,方便后续切换其他缓存实现。
2.2 配置文件
在application.yml中添加缓存配置:
yaml复制spring:
cache:
jcache:
config: classpath:ehcache.xml
然后在resources目录下创建ehcache.xml配置文件:
xml复制<config xmlns='http://www.ehcache.org/v3'>
<cache alias="productCache">
<key-type>java.lang.Long</key-type>
<value-type>com.example.Product</value-type>
<expiry>
<ttl unit="minutes">30</ttl>
</expiry>
<resources>
<heap unit="MB">10</heap>
<offheap unit="MB">50</offheap>
</resources>
</cache>
</config>
这个配置定义了一个名为productCache的缓存:
- 键类型为Long(通常对应实体ID)
- 值类型为Product对象
- 30分钟过期时间
- 10MB堆内存 + 50MB堆外内存
3. 核心实现细节
3.1 启用缓存机制
在Spring Boot启动类上添加注解:
java复制@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3.2 缓存注解使用
Spring提供了几个核心缓存注解:
- @Cacheable - 方法结果缓存
java复制@Cacheable(value = "productCache", key = "#id")
public Product getProductById(Long id) {
// 数据库查询逻辑
return productRepository.findById(id).orElse(null);
}
- @CachePut - 更新缓存
java复制@CachePut(value = "productCache", key = "#product.id")
public Product updateProduct(Product product) {
return productRepository.save(product);
}
- @CacheEvict - 删除缓存
java复制@CacheEvict(value = "productCache", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
3.3 缓存一致性处理
在分布式环境下,本地缓存的最大挑战是数据一致性。我们采用了以下几种策略:
- 数据库变更事件监听:
java复制@TransactionalEventListener(phase = AFTER_COMMIT)
public void handleProductChange(ProductChangeEvent event) {
cacheManager.getCache("productCache").evict(event.getProductId());
}
- 双重检查锁定:
java复制public Product getProductWithLock(Long id) {
Product product = cache.get(id);
if (product == null) {
synchronized (this) {
product = cache.get(id);
if (product == null) {
product = loadFromDB(id);
cache.put(id, product);
}
}
}
return product;
}
4. 高级配置与优化
4.1 多级缓存配置
Ehcache支持三级缓存配置:
xml复制<cache alias="multiLevelCache">
<heap unit="entries">1000</heap>
<offheap unit="MB">100</offheap>
<disk persistent="true" unit="GB">1</disk>
</cache>
这种配置适合热点数据场景:
- 最热数据保存在堆内存(访问最快)
- 次热数据在堆外内存(避免GC压力)
- 冷数据持久化到磁盘(重启不丢失)
4.2 缓存命中率监控
通过JMX可以监控缓存状态:
java复制@Bean
public JCacheManagerCustomizer cacheManagerCustomizer() {
return cm -> {
Caching.getCachingProvider().getCacheManager()
.enableStatistics("productCache", true);
Caching.getCachingProvider().getCacheManager()
.enableManagement("productCache", true);
};
}
然后通过JConsole或VisualVM查看:
- 缓存命中率
- 平均获取时间
- 缓存元素数量
4.3 缓存预热策略
在应用启动时预加载热点数据:
java复制@PostConstruct
public void preloadCache() {
List<Long> hotProductIds = productRepository.findHotProductIds();
hotProductIds.parallelStream()
.forEach(id -> cache.get(id));
}
5. 常见问题与解决方案
5.1 缓存穿透防护
当查询不存在的ID时,会导致大量请求穿透到数据库。解决方案:
java复制@Cacheable(value = "productCache", key = "#id",
unless = "#result == null")
public Product getProductById(Long id) {
Product product = productRepository.findById(id).orElse(null);
if (product == null) {
// 缓存空值,设置较短过期时间
cache.put(id, Product.EMPTY);
}
return product;
}
5.2 缓存雪崩预防
大量缓存同时失效导致数据库压力骤增。解决方案:
xml复制<expiry>
<!-- 基础过期时间30分钟 + 随机0-10分钟 -->
<ttl unit="minutes">30</ttl>
<variance unit="minutes">10</variance>
</expiry>
5.3 大对象缓存优化
对于大对象(如图片、文档),建议:
- 只缓存元数据
- 使用堆外内存
- 启用压缩:
xml复制<resources>
<heap unit="MB">10</heap>
<offheap unit="MB" compressed="true">100</offheap>
</resources>
6. 性能对比测试
我们在测试环境做了基准测试(JMH):
| 场景 | QPS | 平均响应时间 | 数据库负载 |
|---|---|---|---|
| 无缓存 | 1200 | 45ms | 100% |
| Ehcache堆内 | 8500 | 8ms | 15% |
| Ehcache堆外 | 7800 | 9ms | 15% |
| Redis | 3500 | 25ms | 15% |
测试结果表明:
- 本地缓存QPS是Redis的2倍以上
- 响应时间降低到原来的1/5
- 堆内缓存性能略优于堆外
7. 实际应用建议
根据我的项目经验,Ehcache最适合以下场景:
- 读多写少的数据
- 对延迟敏感的业务
- 数据量适中(GB级别以内)
几个实用技巧:
- 对于配置数据,可以设置较长的TTL(如24小时)
- 使用@CacheEvict的allEntries=true清理整个缓存时要谨慎
- 定期检查缓存命中率,低于80%可能需要调整缓存策略
在微服务架构中,建议将本地缓存与分布式缓存结合使用:
- 优先从本地缓存读取
- 本地缓存未命中时查询Redis
- Redis未命中再查数据库
- 数据库变更时通过消息队列通知各节点失效缓存
