1. 概述

本文将介绍 Infinispan——一个功能强大的内存键值存储库。相比同类工具,它提供了更丰富的特性集。我们将通过构建一个实际项目来演示其核心功能,帮助大家理解其工作原理。

2. 项目配置

首先在 pom.xml 中添加依赖(最新版本可在 Maven Central 查询):

<dependency>
    <groupId>org.infinispan</groupId>
    <artifactId>infinispan-core</artifactId>
    <version>9.1.5.Final</version>
</dependency>

后续所有底层基础设施都将通过编程方式处理。

3. CacheManager 配置

CacheManager 是 Infinispan 的核心组件,负责:

  • 托管所有声明的缓存
  • 控制缓存生命周期
  • 管理全局配置

创建方式非常简单:

public DefaultCacheManager cacheManager() {
    return new DefaultCacheManager();
}

现在可以用它来构建缓存了。

4. 缓存配置

缓存由名称和配置定义,使用 ConfigurationBuilder 构建配置。先准备一个模拟耗时查询的测试类:

public class HelloWorldRepository {
    public String getHelloWorld() {
        try {
            System.out.println("执行耗时查询");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // ...
            e.printStackTrace();
        }
        return "Hello World!";
    }
}

Infinispan 通过 @Listener 注解提供事件监听能力。定义监听器后,缓存事件会自动触发通知:

@Listener
public class CacheListener {
    @CacheEntryCreated
    public void entryCreated(CacheEntryCreatedEvent<String, String> event) {
        this.printLog("添加键 '" + event.getKey() + "' 到缓存", event);
    }

    @CacheEntryExpired
    public void entryExpired(CacheEntryExpiredEvent<String, String> event) {
        this.printLog("键 '" + event.getKey() + "' 已过期", event);
    }

    @CacheEntryVisited
    public void entryVisited(CacheEntryVisitedEvent<String, String> event) {
        this.printLog("键 '" + event.getKey() + "' 被访问", event);
    }

    @CacheEntryActivated
    public void entryActivated(CacheEntryActivatedEvent<String, String> event) {
        this.printLog("激活键 '" + event.getKey() + "'", event);
    }

    @CacheEntryPassivated
    public void entryPassivated(CacheEntryPassivatedEvent<String, String> event) {
        this.printLog钝化键 '" + event.getKey() + "'", event);
    }

    @CacheEntryLoaded
    public void entryLoaded(CacheEntryLoadedEvent<String, String> event) {
        this.printLog("加载键 '" + event.getKey() + "'", event);
    }

    @CacheEntriesEvicted
    public void entriesEvicted(CacheEntriesEvictedEvent<String, String> event) {
        StringBuilder builder = new StringBuilder();
        event.getEntries().forEach(
          (key, value) -> builder.append(key).append(", "));
        System.out.println("从缓存驱逐以下条目: " + builder.toString());
    }

    private void printLog(String log, CacheEntryEvent event) {
        if (!event.isPre()) {
            System.out.println(log);
        }
    }
}

⚠️ 注意:打印日志前需检查事件是否已发生,因为某些事件类型会发送两次通知(处理前和处理后)。

缓存创建方法:

private <K, V> Cache<K, V> buildCache(
  String cacheName, 
  DefaultCacheManager cacheManager, 
  CacheListener listener, 
  Configuration configuration) {

    cacheManager.defineConfiguration(cacheName, configuration);
    Cache<K, V> cache = cacheManager.getCache(cacheName);
    cache.addListener(listener);
    return cache;
}

接下来演示五种典型缓存配置:

4.1. 基础缓存

最简单的缓存配置只需一行代码:

public Cache<String, String> simpleHelloWorldCache(
  DefaultCacheManager cacheManager, 
  CacheListener listener) {
    return this.buildCache(SIMPLE_HELLO_WORLD_CACHE, 
      cacheManager, listener, new ConfigurationBuilder().build());
}

使用方式:

public String findSimpleHelloWorld() {
    String cacheKey = "simple-hello";
    return simpleHelloWorldCache
      .computeIfAbsent(cacheKey, k -> repository.getHelloWorld());
}

测试方法(统计执行时间):

protected <T> long timeThis(Supplier<T> supplier) {
    long millis = System.currentTimeMillis();
    supplier.get();
    return System.currentTimeMillis() - millis;
}

验证缓存效果:

@Test
public void whenGetIsCalledTwoTimes_thenTheSecondShouldHitTheCache() {
    assertThat(timeThis(() -> helloWorldService.findSimpleHelloWorld()))
      .isGreaterThanOrEqualTo(1000);

    assertThat(timeThis(() -> helloWorldService.findSimpleHelloWorld()))
      .isLessThan(100);
}

4.2. 过期缓存

为所有条目设置生命周期:

private Configuration expiringConfiguration() {
    return new ConfigurationBuilder().expiration()
      .lifespan(1, TimeUnit.SECONDS)
      .build();
}

缓存创建:

public Cache<String, String> expiringHelloWorldCache(
  DefaultCacheManager cacheManager, 
  CacheListener listener) {
    
    return this.buildCache(EXPIRING_HELLO_WORLD_CACHE, 
      cacheManager, listener, expiringConfiguration());
}

使用示例:

public String findSimpleHelloWorldInExpiringCache() {
    String cacheKey = "simple-hello";
    String helloWorld = expiringHelloWorldCache.get(cacheKey);
    if (helloWorld == null) {
        helloWorld = repository.getHelloWorld();
        expiringHelloWorldCache.put(cacheKey, helloWorld);
    }
    return helloWorld;
}

快速调用测试:

@Test
public void whenGetIsCalledTwoTimesQuickly_thenTheSecondShouldHitTheCache() {
    assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
      .isGreaterThanOrEqualTo(1000);

    assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
      .isLessThan(100);
}

间隔调用测试(验证过期):

@Test
public void whenGetIsCalledTwiceSparsely_thenNeitherHitsTheCache()
  throws InterruptedException {

    assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
      .isGreaterThanOrEqualTo(1000);

    Thread.sleep(1100);

    assertThat(timeThis(() -> helloWorldService.findExpiringHelloWorld()))
      .isGreaterThanOrEqualTo(1000);
}

监听器输出示例:

执行耗时查询
添加键 'simple-hello' 到缓存
键 'simple-hello' 已过期
执行耗时查询
添加键 'simple-hello' 到缓存

✅ 过期触发时机:访问条目或清理线程扫描时。

灵活过期控制

// 单独设置过期时间
simpleHelloWorldCache.put(cacheKey, helloWorld, 10, TimeUnit.SECONDS);

// 设置最大空闲时间
simpleHelloWorldCache.put(cacheKey, helloWorld, -1, TimeUnit.SECONDS, 10, TimeUnit.SECONDS);

4.3. 缓存驱逐

限制缓存条目数量:

private Configuration evictingConfiguration() {
    return new ConfigurationBuilder()
      .memory().evictionType(EvictionType.COUNT).size(1)
      .build();
}

使用方式:

public String findEvictingHelloWorld(String key) {
    String value = evictingHelloWorldCache.get(key);
    if(value == null) {
        value = repository.getHelloWorld();
        evictingHelloWorldCache.put(key, value);
    }
    return value;
}

测试驱逐效果:

@Test
public void whenTwoAreAdded_thenFirstShouldntBeAvailable() {

    assertThat(timeThis(
      () -> helloWorldService.findEvictingHelloWorld("key 1")))
      .isGreaterThanOrEqualTo(1000);

    assertThat(timeThis(
      () -> helloWorldService.findEvictingHelloWorld("key 2")))
      .isGreaterThanOrEqualTo(1000);

    assertThat(timeThis(
      () -> helloWorldService.findEvictingHelloWorld("key 1")))
      .isGreaterThanOrEqualTo(1000);
}

监听器输出:

执行耗时查询
添加键 'key 1' 到缓存
执行耗时查询
从缓存驱逐以下条目: key 1, 
添加键 'key 2' 到缓存
执行耗时查询
从缓存驱逐以下条目: key 2, 
添加键 'key 1' 到缓存

4.4. 钝化缓存

结合驱逐和钝化(Passivation)实现内存优化:

private Configuration passivatingConfiguration() {
    return new ConfigurationBuilder()
      .memory().evictionType(EvictionType.COUNT).size(1)
      .persistence() 
      .passivation(true)    // 启用钝化
      .addSingleFileStore() // 存储到文件
      .purgeOnStartup(true) // 启动时清理
      .location(System.getProperty("java.io.tmpdir")) 
      .build();
}

使用方式:

public String findPassivatingHelloWorld(String key) {
    return passivatingHelloWorldCache.computeIfAbsent(key, k -> 
      repository.getHelloWorld());
}

测试钝化效果:

@Test
public void whenTwoAreAdded_thenTheFirstShouldBeAvailable() {

    assertThat(timeThis(
      () -> helloWorldService.findPassivatingHelloWorld("key 1")))
      .isGreaterThanOrEqualTo(1000);

    assertThat(timeThis(
      () -> helloWorldService.findPassivatingHelloWorld("key 2")))
      .isGreaterThanOrEqualTo(1000);

    assertThat(timeThis(
      () -> helloWorldService.findPassivatingHelloWorld("key 1")))
      .isLessThan(100);
}

监听器输出:

执行耗时查询
添加键 'key 1' 到缓存
执行耗时查询
钝化键 'key 1'
从缓存驱逐以下条目: key 1, 
添加键 'key 2' 到缓存
钝化键 'key 2'
从缓存驱逐以下条目: key 2, 
加载键 'key 1'
激活键 'key 1'
键 'key 1' 被访问

关键流程说明

  1. 钝化:条目被移出内存存储到外部
  2. 驱逐:从内存移除以释放空间
  3. 加载:从外部存储重新加载到内存
  4. 激活:条目在内存中重新可用

4.5. 事务缓存

实现类似数据库的事务控制:

private Configuration transactionalConfiguration() {
    return new ConfigurationBuilder()
      .transaction().transactionMode(TransactionMode.TRANSACTIONAL)
      .lockingMode(LockingMode.PESSIMISTIC)
      .build();
}

测试方法(快速提交 vs 慢速回滚):

public Integer getQuickHowManyVisits() {
    TransactionManager tm = transactionalCache
      .getAdvancedCache().getTransactionManager();
    tm.begin();
    Integer howManyVisits = transactionalCache.get(KEY);
    howManyVisits++;
    System.out.println("尝试设置 HowManyVisits = " + howManyVisits);
    StopWatch watch = new StopWatch();
    watch.start();
    transactionalCache.put(KEY, howManyVisits);
    watch.stop();
    System.out.println("成功设置 HowManyVisits = " + howManyVisits + 
      " 耗时 " + watch.getTotalTimeSeconds() + " 秒");

    tm.commit();
    return howManyVisits;
}
public void startBackgroundBatch() {
    TransactionManager tm = transactionalCache
      .getAdvancedCache().getTransactionManager();
    tm.begin();
    transactionalCache.put(KEY, 1000);
    System.out.println("HowManyVisits 应为 1000,但事务未提交");
    Thread.sleep(1000L);
    tm.rollback();
    System.out.println("慢速事务已回滚");
}

并发测试:

@Test
public void whenLockingAnEntry_thenItShouldBeInaccessible() throws InterruptedException {
    Runnable backGroundJob = () -> transactionalService.startBackgroundBatch();
    Thread backgroundThread = new Thread(backGroundJob);
    transactionalService.getQuickHowManyVisits();
    backgroundThread.start();
    Thread.sleep(100); // 等待后台线程启动

    assertThat(timeThis(() -> transactionalService.getQuickHowManyVisits()))
      .isGreaterThan(500).isLessThan(1000);
}

控制台输出:

添加键 'key' 到缓存
键 'key' 被访问
尝试设置 HowManyVisits = 1
成功设置 HowManyVisits = 1 耗时 0.001 秒
HowManyVisits 应为 1000,但事务未提交
键 'key' 被访问
尝试设置 HowManyVisits = 2
成功设置 HowManyVisits = 2 耗时 0.902 秒
慢速事务已回滚

❌ 注意:主线程因慢速事务持有锁而等待近1秒。

5. 总结

本文介绍了 Infinispan 的核心特性及实际应用场景,包括:

  • 基础缓存操作
  • 条目过期控制
  • 内存驱逐策略
  • 钝化机制
  • 事务管理

完整代码示例可在 GitHub 获取。


原始标题:A Guide to Infinispan in Java