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
CharSequence
、String
、StringBuilder
Integer
、Long
、Double
- 实现
Externalizable
的类(需有无参构造函数) - 实现
Serializable
的类(包括基本类型包装类和数组) - 自定义类型(需提供自定义序列化器)
7. 查询操作
Chronicle Map 支持单 key 和多 key 查询。
7.1 单 Key 查询
支持标准的 Map
和 ConcurrentMap
接口操作,例如:
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 项目。