EBEasyBuild Docs
文档/后端/Chronicle Map

easyfk-chronicle-map Chronicle Map

Chronicle Map — 堆外高性能键值存储阅读时间 ~10 min

1. 模块概述

chronicle-map 是 EasyFK 框架中基于 Chronicle Map 的高性能堆外键值存储组件。Chronicle Map 是一个开源的嵌入式键值存储引擎,数据存储在堆外内存(Off-Heap),不受 JVM GC 影响,同时支持文件持久化,进程重启后数据可自动恢复。

该模块提供了完整的自动配置、类型安全的 Map 管理器和便捷的操作模板,开发者只需通过 YAML 配置即可创建和使用高性能键值存储,适用于本地缓存、会话存储、配置中心、计数器等场景。

2. 依赖引入

Maven

xml
<dependency>
    <groupId>com.mcst</groupId>
    <artifactId>chronicle-map</artifactId>
</dependency>

Gradle

gradle
dependencies {
    implementation 'com.mcst:chronicle-map'
}
TIP
版本号由框架统一 BOM 管理,无需手动指定。

该模块会自动传递引入以下依赖:

  • net.openhft:chronicle-map — Chronicle Map 核心库

3. 工作原理

Chronicle Map 基于内存映射文件(Memory-Mapped File)技术,将数据存储在堆外内存中:

plaintext
┌─────────────────────────────────────────────────┐
│                  应用业务层                       │
│  ChronicleMapTemplate.put / get / remove         │
└──────────────────────┬──────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────┐
│  ChronicleMapManager(Map 管理器)                │
│  · 类型安全校验 · 读写锁优化 · 懒加载创建        │
│  · 类型验证缓存 · 类型化包装器缓存               │
└──────────────────────┬──────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────┐
│  Chronicle Map(堆外键值存储引擎)                │
│  · 堆外内存(Off-Heap)· 零 GC 影响             │
│  · 内存映射文件 · 持久化 / 恢复                  │
│  · 多线程并发安全 · 亚微秒级读写                 │
└──────────────────────┬──────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────┐
│  文件系统(可选持久化)                           │
│  ./chronicle-maps/mapName.dat                    │
└─────────────────────────────────────────────────┘
  • 堆外存储:数据不在 JVM 堆内,不会触发 GC,适合大容量缓存
  • 内存映射文件:通过 mmap 将文件映射到内存,读写性能接近内存操作
  • 持久化支持:数据可持久化到文件,进程重启后通过 recoverPersistedTo 自动恢复
  • 多线程安全:Chronicle Map 内部保证线程安全,无需外部加锁

4. 自动配置

模块通过 Spring Boot 自动配置机制注册以下 Bean:

Bean类型条件说明
ChronicleMapManagerManager@ConditionalOnMissingBean + @LazyMap 管理器,懒加载初始化
ChronicleMapTemplateTemplate@ConditionalOnMissingBean操作模板,依赖 Manager
TIP
懒加载ChronicleMapManager 使用 @Lazy 注解,只在首次使用时初始化,避免启动时不必要的资源消耗。

自动配置流程

  1. 读取 easyfk.config.chronicle.map 前缀的配置属性
  2. 创建 ChronicleMapManager 实例
  3. 遍历 maps 配置列表,为每个预定义的 Map 创建 Chronicle Map 实例并注册
  4. 创建 ChronicleMapTemplate 实例

5. 配置说明

5.1 全局配置属性

配置前缀:easyfk.config.chronicle.map

属性类型默认值说明
default-pathString./chronicle-maps默认存储目录
default-max-entrieslong100000默认最大条目数
default-average-key-sizedouble64.0默认平均键大小(字节)
default-average-value-sizedouble1024.0默认平均值大小(字节)
default-file-extensionString.dat默认持久化文件扩展名
persistence-enabledbooleantrue是否启用持久化
recover-on-startupbooleantrue启动时是否恢复数据
compression-enabledbooleanfalse是否启用压缩
apply-average-sizesbooleantrue是否应用平均大小参数
fixed-size-optimization-enabledbooleantrue是否启用固定尺寸类型优化

5.2 预定义 Map 配置

通过 maps 列表预定义需要在启动时创建的 Chronicle Map:

yaml
easyfk:
  config:
    chronicle:
      map:
        default-path: ./chronicle-maps
        default-max-entries: 100000
        default-average-key-size: 64
        default-average-value-size: 1024
        persistence-enabled: true
        maps:
          - map-name: userCache
            max-entries: 50000
            average-key-size: 32
            average-value-size: 512
            persistence-enabled: true
            key-type: java.lang.String
            value-type: java.lang.Object
          - map-name: sessionStore
            max-entries: 10000
            average-key-size: 64
            average-value-size: 2048
            persistence-enabled: true
          - map-name: counterMap
            max-entries: 1000
            key-type: java.lang.String
            value-type: java.lang.Long
            persistence-enabled: false

5.3 单个 Map 配置项

属性类型默认值说明
map-nameString必填Map 名称,全局唯一标识
file-pathString自动生成持久化文件路径,未配置时按 defaultPath/mapName.ext 生成
max-entriesLong使用全局默认最大条目数
average-key-sizeDouble使用全局默认平均键大小(字节)
average-value-sizeDouble使用全局默认平均值大小(字节)
persistence-enabledbooleantrue是否启用持久化
compression-enabledbooleanfalse是否启用压缩
key-typeClassString.class键的 Java 类型
value-typeClassObject.class值的 Java 类型
file-extensionString使用全局默认文件扩展名
apply-average-sizesBoolean使用全局默认是否应用平均大小参数
fixed-size-optimization-enabledBoolean使用全局默认是否启用固定尺寸类型优化
TIP
固定尺寸类型优化:当 key 或 value 类型为 LongIntegerDouble 等基本类型包装类时,Chronicle Map 已知其精确大小,无需设置 averageKeySize / averageValueSize,启用该优化可自动跳过。

6. 使用指南

6.1 使用 ChronicleMapTemplate(推荐)

ChronicleMapTemplate 提供简洁的 API,适合大部分使用场景:

java
@Service
public class UserCacheService {

    @Autowired
    private ChronicleMapTemplate chronicleMapTemplate;

    private static final String MAP_NAME = "userCache";

    // 存储
    public void cacheUser(String userId, UserDTO user) {
        chronicleMapTemplate.put(MAP_NAME, userId, user);
    }

    // 获取
    public UserDTO getUser(String userId) {
        return (UserDTO) chronicleMapTemplate.get(MAP_NAME, userId);
    }

    // 获取(带默认值)
    public UserDTO getUserOrDefault(String userId, UserDTO defaultUser) {
        return (UserDTO) chronicleMapTemplate.getOrDefault(MAP_NAME, userId, defaultUser);
    }

    // 不存在时才存储
    public void cacheIfAbsent(String userId, UserDTO user) {
        chronicleMapTemplate.putIfAbsent(MAP_NAME, userId, user);
    }

    // 删除
    public void removeUser(String userId) {
        chronicleMapTemplate.remove(MAP_NAME, userId);
    }

    // 检查是否存在
    public boolean exists(String userId) {
        return chronicleMapTemplate.containsKey(MAP_NAME, userId);
    }

    // 获取缓存大小
    public long cacheSize() {
        return chronicleMapTemplate.size(MAP_NAME);
    }
}

6.2 批量操作

java
// 批量存储
Map<String, Object> batch = new HashMap<>();
batch.put("user:1001", user1);
batch.put("user:1002", user2);
batch.put("user:1003", user3);
chronicleMapTemplate.putAll("userCache", batch);

// 获取所有键
Set<String> keys = chronicleMapTemplate.keySet("userCache");

// 获取所有值
Collection<Object> values = chronicleMapTemplate.values("userCache");

// 获取所有键值对
Set<Map.Entry<String, Object>> entries = chronicleMapTemplate.entrySet("userCache");

// 清空
chronicleMapTemplate.clear("userCache");

6.3 计算操作

java
// 如果键不存在,计算并存储
Object value = chronicleMapTemplate.computeIfAbsent("configCache", "db.url",
    key -> loadConfigFromDB(key));

// 如果键存在,重新计算
chronicleMapTemplate.computeIfPresent("counterMap", "loginCount",
    (key, oldValue) -> (Long) oldValue + 1);

6.4 强类型操作

对于已知类型的 Map,使用强类型 API 避免类型转换:

java
@Service
public class CounterService {

    @Autowired
    private ChronicleMapTemplate chronicleMapTemplate;

    private static final String MAP_NAME = "counterMap";

    // 强类型存储
    public void setCounter(String name, Long value) {
        chronicleMapTemplate.put(MAP_NAME, String.class, Long.class, name, value);
    }

    // 强类型获取
    public Long getCounter(String name) {
        return chronicleMapTemplate.get(MAP_NAME, String.class, Long.class, name);
    }

    // 强类型批量存储
    public void setCounters(Map<String, Long> counters) {
        chronicleMapTemplate.putAll(MAP_NAME, String.class, Long.class, counters);
    }

    // 强类型原子计算
    public Long incrementCounter(String name) {
        return chronicleMapTemplate.computeIfPresent(MAP_NAME, String.class, Long.class,
            name, (k, v) -> v + 1);
    }
}

6.5 使用 ChronicleMapManager(高级)

需要更精细控制时,可直接使用 ChronicleMapManager

java
@Service
public class DynamicMapService {

    @Autowired
    private ChronicleMapManager chronicleMapManager;

    // 动态创建 Map
    public void createMap(String mapName) {
        chronicleMapManager.getOrCreateMap(mapName, String.class, String.class);
    }

    // 带自定义配置创建
    public void createCustomMap(String mapName) {
        chronicleMapManager.getOrCreateMap(mapName,
            50000,   // maxEntries
            32.0,    // avgKeySize
            256.0,   // avgValueSize
            "./data/" + mapName + ".dat"  // filePath
        );
    }

    // 检查 Map 是否存在
    public boolean mapExists(String mapName) {
        return chronicleMapManager.containsMap(mapName);
    }

    // 获取所有 Map 名称
    public Set<String> listMaps() {
        return chronicleMapManager.getMapNames();
    }

    // 获取统计信息
    public String getStats(String mapName) {
        return chronicleMapManager.getMapStats(mapName);
    }

    // 移除并关闭 Map
    public void removeMap(String mapName) {
        chronicleMapManager.removeMap(mapName);
    }

    // 关闭所有 Map(应用关闭时)
    public void shutdown() {
        chronicleMapManager.closeAll();
    }
}

6.6 Map 管理操作

java
// 检查 Map 是否存在
boolean exists = chronicleMapTemplate.mapExists("userCache");

// 获取所有 Map 名称
Set<String> mapNames = chronicleMapTemplate.getAllMapNames();

// 获取统计信息
String stats = chronicleMapTemplate.getStats("userCache");

7. 容量规划

Chronicle Map 需要在创建时预估数据规模,合理的容量规划对性能至关重要:

7.1 maxEntries(最大条目数)

  • 设置预期的最大键值对数量
  • 建议按预期峰值的 1.5 ~ 2 倍配置,留有余量
  • 超过 maxEntries 后仍可写入,但性能会下降

7.2 averageKeySize / averageValueSize(平均大小)

  • 对于 String 类型的 key,按平均字符串长度估算字节数
  • 对于序列化对象的 value,按序列化后的平均字节数估算
  • 估算偏小会导致频繁 resize,估算偏大会浪费内存

7.3 示例

场景maxEntriesavgKeySizeavgValueSize
用户信息缓存100,00032512
会话存储10,000642048
配置中心1,00064256
计数器10,000328(Long)
限流令牌桶50,00064128

8. 持久化与恢复

8.1 持久化模式

yaml
easyfk:
  config:
    chronicle:
      map:
        persistence-enabled: true    # 全局启用持久化
        default-path: ./chronicle-maps
  • 启用持久化后,数据自动写入磁盘文件
  • 进程重启时通过 recoverPersistedTo 自动恢复数据
  • 数据文件默认存储在 ./chronicle-maps/ 目录下

8.2 纯内存模式

yaml
easyfk:
  config:
    chronicle:
      map:
        persistence-enabled: false   # 纯内存模式
  • 纯内存模式下不创建持久化文件
  • 进程重启后数据丢失
  • 适合临时缓存、计数器等不需要持久化的场景

8.3 混合模式

可以为不同的 Map 分别设置持久化策略:

yaml
easyfk:
  config:
    chronicle:
      map:
        persistence-enabled: true
        maps:
          - map-name: importantData
            persistence-enabled: true    # 重要数据持久化
          - map-name: tempCache
            persistence-enabled: false   # 临时缓存不持久化

9. 最佳实践

  1. 容量预估:创建 Map 时合理估算 maxEntriesaverageKeySizeaverageValueSize,避免频繁 resize 或内存浪费。
  2. 类型安全:使用强类型 API(传入 keyType / valueType),避免运行时类型转换错误。
  3. 预定义 Map:常用的 Map 通过 YAML 配置预定义,应用启动时自动创建,避免首次访问时的创建延迟。
  4. 持久化策略:重要数据启用持久化,临时缓存使用纯内存模式,按需选择。
  5. Serializable:存储的对象需实现 Serializable 接口,确保可序列化。
  6. 资源清理:应用关闭时调用 ChronicleMapManager.closeAll() 关闭所有 Map,释放堆外内存。
  7. 固定尺寸优化:对于 LongInteger 等基本类型,启用 fixed-size-optimization-enabled 可跳过不必要的平均大小设置。
  8. 避免超大 Value:Chronicle Map 适合存储中小型数据,超大对象(>1MB)建议使用文件存储或对象存储。
  9. 监控统计:通过 getStats() 定期监控 Map 的大小和健康状态。
  10. 动态创建:运行时通过 ChronicleMapManager.getOrCreateMap() 动态创建 Map,自动处理持久化文件和类型注册。

easyfk-chronicle-map — 堆外高性能键值存储,突破 JVM 内存限制。

— END —