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),不是像 HazelcastInfispan 那样的分布式缓存。别踩坑,别指望它跨 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


原始标题:Introduction to cache2k