在“高并发 + 低延迟”的战场里,远程缓存(Redis/Memcached)总要走一次网络,1 ms 起跳;而 EhCache 与业务线程同堆同进程,get/put 只需 几十纳秒。 如果:
那么 EhCache 就是性能性价比最高的解决方案。下面把两年踩过的坑与最佳实践一次性输出。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.10.8</version>
</dependency>
spring:
cache:
type: ehcache
ehcache:
config: classpath:ehcache.xml
业务代码:
@Service
public class OrderService {
@Cacheable(value = "order", key = "#id")
public Order getOrder(Long id) {
return dao.selectById(id); // 2s 的 SQL
}
}
启动后控制台出现:
org.ehcache.core.EhcacheManager - Cache 'order' created in EhcacheManager.
第一次调用 2s,第二次 0.02 ms,命中率 100 % —— 性能直接提升 5 个数量级。
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.ehcache.org/v3"
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">
<!-- ① 默认模板:30 min 过期 + 1000 条 + 堆 32 MB -->
<cache-template name="default">
<expiry>
<ttl unit="minutes">30</ttl>
</expiry>
<heap unit="entries">1000</heap>
</cache-template>
<!-- ② 订单缓存:热点数据,磁盘溢出,重启可恢复 -->
<cache alias="order" uses-template="default">
<heap unit="MB">64</heap>
<disk unit="MB">512</disk> <!-- 需开启 <persistence> -->
<expiry>
<ttl unit="minutes">15</ttl>
</expiry>
</cache>
<!-- ③ 字典缓存:基本不变,永久有效 -->
<cache alias="dict">
<heap unit="entries">5000</heap>
<expiry>
<none/>
</expiry>
</cache>
<!-- ④ 磁盘持久化目录 -->
<persistence directory="spring.tmpdir/caches"/>
</config>
注意:磁盘溢出需额外依赖
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache-transactions</artifactId>
<version>3.10.8</version>
</dependency>
需求 | EhCache 原生方案 | 代码示例 |
|---|---|---|
缓存漂移(集群多节点) | 结合 Kafka/Redis Pub/Sub 广播失效事件 | 见 4.1 |
局部刷新(避免雪崩) | ExpiryPolicyBuilder.timeToIdle() + ScheduledThreadPool 异步刷新 | 见 4.2 |
大对象(> 1 MB) | 开启 off-heap 防止老年代膨胀 | 见 4.3 |
可视化 | JMX + Prometheus 导出 CacheStatistics | 见 4.4 |
场景:节点 A 更新订单,节点 B/C 必须立即感知。
思路:利用 CacheEventListener 发布失效消息,其他节点消费并 clear()。
@Component
public class CacheEventPublisher implements CacheEventListener<Object, Object> {
@Autowired
private KafkaTemplate<String, String> kafka;
@Override
public void onEvent(CacheEvent<?, ?> event) {
if (event.getType() == EventType.REMOVED) {
kafka.send("cache-invalid", event.getKey().toString());
}
}
}
消费端:
@KafkaListener(topics = "cache-invalid")
public void receive(String key) {
cacheManager.getCache("order").evict(key);
}
实测 99 % 一致性 < 200 ms,剩余 1 % 由兜底 TTL 保证。
EhCache 没有 refreshAfterWrite,但可以用 TimeToIdle 模拟:
CacheConfigurationBuilder<Long, Order> cfg =
CacheConfigurationBuilder.newCacheConfigurationBuilder(
Long.class, Order.class,
ResourcePoolsBuilder.heap(1000))
.withExpiry(ExpiryPolicyBuilder.timeToIdle(Duration.ofMinutes(5)))
.build();
// 异步刷新线程
Executors.newSingleThreadScheduledExecutor()
.scheduleWithFixedDelay(() -> {
Long hotKey = findHotKey();
Order fresh = dao.selectById(hotKey);
cache.put(hotKey, fresh);
}, 0, 3, TimeUnit.MINUTES);
效果:
ResourcePoolsBuilder.newResourcePoolsBuilder()
.heap(100, MemoryUnit.MB) // on-heap 缓存索引
.offheap(2, MemoryUnit.GB); // 大对象本体
注意:off-heap 必须 序列化,推荐 Kryo 或 Protobuf,比 Java 序列化快 10 倍。
@Bean
public CacheStatisticsRecorder statisticsRecorder() {
return new DefaultCacheStatisticsRecorder();
}
// Prometheus exporter
new EhCacheStatisticsCollector(cacheManager).register();
面板核心指标:
cache_get_total / cache_hit_total → 实时命中率cache_eviction_total → 是否“过早”淘汰cache_put_rate → 热点探测“进程内缓存不是 Redis 的替代品,而是 CPU L3 缓存的延伸。”
把最热、最小、最延迟敏感的数据放在 EhCache, 把海量、共享、事务型数据交给 Redis, 把冷数据留给 DB, 这才是缓存架构的 黄金三角。