1. 概述
本文将介绍 cache2k —— 一个轻量级、高性能的 Java 内存缓存库。它主打低延迟和高并发访问,适合对性能敏感的场景。如果你正在寻找一个比 ConcurrentHashMap
更强大、又不像分布式缓存那样重的本地缓存方案,cache2k 是个不错的选择。
2. 关于 cache2k
cache2k 的核心优势在于其非阻塞、无锁(wait-free)的设计,使得在高并发下依然能保持极低的访问延迟。✅
它不仅性能强悍,还提供了丰富的功能特性,包括:
- ✅ 线程安全的原子操作
- ✅ 支持带阻塞的缓存加载(read-through)
- ✅ 自动过期(expiry)与预刷新(refresh-ahead)
- ✅ 事件监听器(event listeners)
- ✅ 兼容 JCache(JSR107)标准
- ✅ 与 Spring、Hibernate、Datanucleus 等主流框架集成
⚠️ 需要注意的是,cache2k 是本地缓存(in-memory),不是像 Hazelcast 或 Infispan 那样的分布式缓存。别踩坑,别指望它跨 JVM 共享数据。
3. Maven 依赖
使用 cache2k 前,先引入 BOM 来统一管理版本:
<dependency>
<groupId>org.cache2k</groupId>
<artifactId>cache2k-base-bom</artifactId>
<version>1.2.3.Final</version>
<type>pom</type>
</dependency>
接着就可以直接引入核心模块(无需指定版本):
<dependency>
<groupId>org.cache2k</groupId>
<artifactId>cache2k-api</artifactId>
</dependency>
<dependency>
<groupId>org.cache2k</groupId>
<artifactId>cache2k-core</artifactId>
</dependency>
4. 一个简单的 cache2k 示例
我们通过一个电商折扣计算的场景来快速上手。
假设网站对运动类商品打 20% 折扣,其他商品打 10%。为了避免每次请求都重复计算,我们将折扣结果缓存起来。
4.1 手动缓存(Cache-Aside)
先看传统的“缓存旁路”模式实现:
public class ProductHelper {
private Cache<String, Integer> cachedDiscounts;
private int cacheMissCount = 0;
public ProductHelper() {
cachedDiscounts = Cache2kBuilder.of(String.class, Integer.class)
.name("discount")
.eternal(true)
.entryCapacity(100)
.build();
}
public Integer getDiscount(String productType) {
Integer discount = cachedDiscounts.get(productType);
if (Objects.isNull(discount)) {
cacheMissCount++;
discount = "Sports".equalsIgnoreCase(productType) ? 20 : 10;
cachedDiscounts.put(productType, discount);
}
return discount;
}
// Getters and setters
}
关键配置说明:
配置项 | 说明 |
---|---|
.name("discount") |
设置缓存名称,便于监控和管理,非必需 |
.eternal(true) |
禁用自动过期,缓存永不过期(直到容量满被驱逐) |
.entryCapacity(100) |
最多缓存 100 个条目,满了触发 LRU 驱逐 |
测试验证缓存命中:
@Test
public void whenInvokedGetDiscountTwice_thenGetItFromCache() {
ProductHelper productHelper = new ProductHelper();
assertTrue(productHelper.getCacheMissCount() == 0);
assertTrue(productHelper.getDiscount("Sports") == 20);
assertTrue(productHelper.getDiscount("Sports") == 20);
assertTrue(productHelper.getCacheMissCount() == 1);
}
第二次调用命中缓存,cacheMissCount
不再增加,✅ 验证通过。
5. cache2k 核心特性
接下来我们逐步升级这个例子,体验 cache2k 更强大的功能。
5.1 配置缓存过期(Expiry)
现在需求变了:运动类折扣只在 10ms 内有效(测试用,实际场景可能是几分钟或几小时)。我们可以用 expireAfterWrite
实现写后过期:
cachedDiscounts = Cache2kBuilder.of(String.class, Integer.class)
.name("discount")
.entryCapacity(100)
.expireAfterWrite(10, TimeUnit.MILLISECONDS) // ⚠️ 移除了 eternal(true)
.build();
测试过期逻辑:
@Test
public void whenInvokedGetDiscountAfterExpiration_thenDiscountCalculatedAgain()
throws InterruptedException {
ProductHelper productHelper = new ProductHelper();
assertTrue(productHelper.getCacheMissCount() == 0);
assertTrue(productHelper.getDiscount("Sports") == 20);
assertTrue(productHelper.getCacheMissCount() == 1);
Thread.sleep(20); // 等待过期
assertTrue(productHelper.getDiscount("Sports") == 20);
assertTrue(productHelper.getCacheMissCount() == 2); // ✅ 增加了,说明重新计算
}
对于更复杂的过期策略(比如根据值动态决定过期时间),可以实现 ExpiryPolicy
接口。
5.2 缓存加载(Read-Through)
上面的例子是手动维护缓存(cache-aside),代码略显啰嗦。cache2k 支持 read-through 模式,由缓存自己负责加载缺失的值,代码更简洁。
改造如下:
cachedDiscounts = Cache2kBuilder.of(String.class, Integer.class)
.name("discount")
.entryCapacity(100)
.expireAfterWrite(10, TimeUnit.MILLISECONDS)
.loader((key) -> {
cacheMissCount++;
return "Sports".equalsIgnoreCase(key) ? 20 : 10;
})
.build();
getDiscount
方法变得极其简单:
public Integer getDiscount(String productType) {
return cachedDiscounts.get(productType); // ✅ 缓存自动调用 loader
}
测试验证:
@Test
public void whenInvokedGetDiscount_thenPopulateCacheUsingLoader() {
ProductHelper productHelper = new ProductHelper();
assertTrue(productHelper.getCacheMissCount() == 0);
assertTrue(productHelper.getDiscount("Sports") == 20);
assertTrue(productHelper.getCacheMissCount() == 1);
assertTrue(productHelper.getDiscount("Electronics") == 10);
assertTrue(productHelper.getCacheMissCount() == 2);
}
✅ 逻辑清晰,职责分离,推荐在新项目中使用这种方式。
5.3 事件监听器(Event Listeners)
有时我们需要在缓存操作时执行一些副作用,比如记录日志、监控指标等。cache2k 提供了事件监听机制。
例如,监听缓存条目创建事件:
.addListener(new CacheEntryCreatedListener<String, Integer>() {
@Override
public void onEntryCreated(Cache<String, Integer> cache, CacheEntry<String, Integer> entry) {
LOGGER.info("Entry created: [{}, {}].", entry.getKey(), entry.getValue());
}
})
运行测试后,日志输出:
Entry created: [Sports, 20].
⚠️ 注意:除过期事件外,其他事件监听器默认是同步执行的,会影响 get/put 性能。如果希望异步执行,使用 .addAsyncListener()
。
5.4 原子操作
cache2k 的 Cache
接口提供了一系列原子方法,适用于单个条目的并发安全操作,避免 ABA 问题。
常用方法包括:
putIfAbsent(K key, V value)
:键不存在时才放入replaceIfEquals(K key, V expect, V update)
:旧值匹配时才替换removeIfEquals(K key, V value)
:值匹配时才删除peekAndPut(K key, V value)
:获取旧值并放入新值(原子)peekAndReplace(K key, V value)
:获取旧值并替换为新值(原子)
这些方法在高并发下非常有用,比如实现简单的分布式锁或计数器。
6. 总结
cache2k 是一个高性能、功能全面的本地缓存库,特别适合对延迟敏感的应用。它提供了自动加载、过期、事件监听、原子操作等实用特性,API 设计简洁直观。
对于 Spring 用户,还可以结合 @Cacheable
注解无缝使用。建议在新项目中优先考虑 cache2k,替代 Guava Cache 或手写 ConcurrentHashMap 缓存。
更多高级用法可参考 cache2k 官方用户指南。
完整代码示例已上传至 GitHub:https://github.com/yourname/tutorials/tree/master/libraries-data-3