1. 概述

本文将介绍如何使用 Chronicle Map 实现高效的键值对存储。我们将通过几个小例子来展示其行为与使用方式。

Chronicle Map 是一个高性能的内存外键值存储库,适用于低延迟或跨进程通信的场景。


2. 什么是 Chronicle Map?

根据官方文档描述:

“Chronicle Map 是一个超高速、内存外、非阻塞的键值存储,专为低延迟和/或多进程应用设计。”

简而言之,它是一个 off-heap(堆外内存)的键值存储结构。不需要大量堆内存即可运行,其容量可以根据磁盘空间动态扩展。此外,它还支持多主架构下的数据复制。

接下来我们将学习如何创建和使用它。


3. Maven 依赖

要使用 Chronicle Map,首先在项目中引入对应的 Maven 依赖:

<dependency>
    <groupId>net.openhft</groupId>
    <artifactId>chronicle-map</artifactId>
    <version>3.24ea1</version>
</dependency>

如果你使用的是 JDK 11 或更高版本,需要为 JVM 添加额外参数,具体可参考官方文档说明。

为了测试顺利运行,建议在 maven-surefire-plugin 中添加如下配置:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>${maven-surefire-plugin.version}</version>
            <configuration>
                <argLine>--add-exports=java.base/jdk.internal.ref=ALL-UNNAMED 
                  --add-exports=java.base/sun.nio.ch=ALL-UNNAMED 
                  --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED 
                  --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED 
                  --add-opens=jdk.compiler/com.sun.tools.javac=ALL-UNNAMED 
                  --add-opens=java.base/java.lang=ALL-UNNAMED 
                  --add-opens=java.base/java.lang.reflect=ALL-UNNAMED 
                  --add-opens=java.base/java.io=ALL-UNNAMED 
                  --add-opens=java.base/java.util=ALL-UNNAMED</argLine>
            </configuration>
        </plugin>
    </plugins>
</build>

4. Chronicle Map 的类型

Chronicle Map 支持两种创建方式:

  • 内存型(In-Memory)
  • 持久化型(Persisted)

下面我们分别介绍。

4.1 内存型 Map

内存型 Map 存储在 JVM 的堆外内存中,只能在创建它的 JVM 进程内访问。示例如下:

ChronicleMap<LongValue, CharSequence> inMemoryCountryMap = ChronicleMap
  .of(LongValue.class, CharSequence.class)
  .name("country-map")
  .entries(50)
  .averageValue("America")
  .create();

这里我们创建了一个用于存储国家 ID 和名称的 Map。注意其中的 averageValue() 方法用于告诉 Chronicle Map 每个值的大致字节数,以优化内存分配。

⚠️ 注意: 内存型 Map 的数据在 JVM 关闭后会被清除。

4.2 持久化型 Map

持久化型 Map 会将数据写入磁盘,即使 JVM 退出也不会丢失。示例如下:

ChronicleMap<LongValue, CharSequence> persistedCountryMap = ChronicleMap
  .of(LongValue.class, CharSequence.class)
  .name("country-map")
  .entries(50)
  .averageValue("America")
  .createPersistedTo(new File(System.getProperty("user.home") + "/country-details.dat"));

该代码会在用户目录下生成一个名为 country-details.dat 的文件。如果文件已存在,则会加载已有数据。

持久化 Map 适用于以下场景:

✅ 支持热更新(hot redeployment)
✅ 多进程共享数据
✅ 需要持久化存储的场景


5. 容量配置

创建 Chronicle Map 时,必须指定 key 和 value 的平均大小(除非使用基本类型或 value 接口)

在上面的例子中我们没有配置 key 的平均值,因为 LongValue 是一个 value 接口,会自动处理大小。

常用的配置方法包括:

方法名 用途
averageValue() 提供一个示例值,用于估算平均字节数
averageValueSize() 直接指定平均字节数
constantValueSizeBySample() 指定固定大小(适用于所有值大小一致的情况)
averageKey() 类似于 value,用于 key 的平均大小估算
averageKeySize() 直接指定 key 的平均字节数
constantKeySizeBySample() 指定固定 key 大小

6. 支持的 Key 和 Value 类型

Chronicle Map 对 key 和 value 的类型有一定要求。以下是推荐使用的类型:

  • Value 接口(Chronicle Values 提供)
  • 实现 Byteable 接口的类(来自 Chronicle Bytes)
  • 实现 BytesMarshallable 接口的类(需有无参构造函数)
  • byte[]ByteBuffer
  • CharSequenceStringStringBuilder
  • IntegerLongDouble
  • 实现 Externalizable 的类(需有无参构造函数)
  • 实现 Serializable 的类(包括基本类型包装类和数组)
  • 自定义类型(需提供自定义序列化器)

7. 查询操作

Chronicle Map 支持单 key 和多 key 查询。

7.1 单 Key 查询

支持标准的 MapConcurrentMap 接口操作,例如:

LongValue qatarKey = Values.newHeapInstance(LongValue.class);
qatarKey.setValue(1);
inMemoryCountryMap.put(qatarKey, "Qatar");

CharSequence country = inMemoryCountryMap.get(qatarKey);

除此之外,还有一个高效方法 getUsing(),用于减少内存分配开销:

LongValue key = Values.newHeapInstance(LongValue.class);
StringBuilder country = new StringBuilder();
key.setValue(1);
persistedCountryMap.getUsing(key, country);
assertThat(country.toString(), is("Romania"));

key.setValue(2);
persistedCountryMap.getUsing(key, country);
assertThat(country.toString(), is("India"));

这个方法会复用传入的 country 对象,避免频繁创建新对象,非常适合高频查询场景。

7.2 多 Key 查询

当需要同时操作多个 key 时,可以使用 queryContext() 方法,它提供了对 Map 条目操作的上下文支持。

示例代码如下:

Set<Integer> averageValue = IntStream.of(1, 2).boxed().collect(Collectors.toSet());
ChronicleMap<Integer, Set<Integer>> multiMap = ChronicleMap
  .of(Integer.class, (Class<Set<Integer>>) (Class) Set.class)
  .name("multi-map")
  .entries(50)
  .averageValue(averageValue)
  .create();

Set<Integer> set1 = new HashSet<>();
set1.add(1);
set1.add(2);
multiMap.put(1, set1);

Set<Integer> set2 = new HashSet<>();
set2.add(3);
multiMap.put(2, set2);

接下来使用 queryContext() 同时操作两个 key:

try (ExternalMapQueryContext<Integer, Set<Integer>, ?> fistContext = multiMap.queryContext(1)) {
    try (ExternalMapQueryContext<Integer, Set<Integer>, ?> secondContext = multiMap.queryContext(2)) {
        fistContext.updateLock().lock();
        secondContext.updateLock().lock();

        MapEntry<Integer, Set<Integer>> firstEntry = fistContext.entry();
        Set<Integer> firstSet = firstEntry.value().get();
        firstSet.remove(2);

        MapEntry<Integer, Set<Integer>> secondEntry = secondContext.entry();
        Set<Integer> secondSet = secondEntry.value().get();
        secondSet.add(4);

        firstEntry.doReplaceValue(fistContext.wrapValueAsData(firstSet));
        secondEntry.doReplaceValue(secondContext.wrapValueAsData(secondSet));
    }
} finally {
    assertThat(multiMap.get(1).size(), is(1));
    assertThat(multiMap.get(2).size(), is(2));
}

⚠️ 注意: 在多 key 操作中,需要加锁以防止并发写冲突。


8. 关闭 Chronicle Map

使用完后,记得调用 close() 方法释放资源:

persistedCountryMap.close();
inMemoryCountryMap.close();
multiMap.close();

⚠️ 踩坑提醒: 所有操作必须在关闭 Map 之前完成,否则可能导致 JVM 崩溃。


9. 总结

Chronicle Map 是一个高性能的键值存储工具,适用于低延迟和多进程通信场景。它支持内存型和持久化型 Map,具备灵活的容量配置和丰富的查询接口。

虽然社区版已提供大部分核心功能,但企业版还支持如跨服务器数据复制、远程调用等高级特性。

完整示例代码请参考 GitHub 项目


原始标题:Key Value Store with Chronicle Map