1. 概述

MicroStream专为JVM设计的对象图持久化引擎。我们可以用它来存储Java对象图并将其恢复到内存中。通过自定义序列化机制,MicroStream能够存储任意Java类型,支持加载完整对象图、部分子图或单个对象。

本文将首先探讨开发这种对象图持久化引擎的动机,然后对比传统关系型数据库和标准Java序列化。最后演示如何创建对象图存储,并实现数据的持久化、加载和删除操作,以及如何通过本地内存和原生Java API查询数据。

2. 对象关系不匹配问题

大多数Java项目都需要某种形式的数据库存储,但Java与主流关系型/NoSQL数据库的数据结构存在差异。因此我们需要在Java对象和数据库结构之间建立映射,这既需要开发工作量,也消耗执行时间。例如,在关系型数据库中需要将实体映射到表,属性映射到字段。

从数据库加载数据时,常需执行复杂的多表SQL查询。虽然Hibernate等ORM框架能帮助开发者弥合这个鸿沟,但在复杂场景下,框架生成的查询往往无法完全优化。

MicroStream通过在内存操作和数据持久化中使用完全相同的数据结构,从根本上解决了这个不匹配问题。

3. 以JVM为存储核心

MicroStream直接利用JVM作为存储层,实现基于纯Java的快速内存数据处理。它摒弃了外部存储方案,提供了现代化的原生数据存储库。

3.1. 数据库管理系统定位

MicroStream是持久化引擎而非数据库管理系统(DBMS)。它有意省略了用户管理、连接管理、会话处理等标准DBMS功能,专注于提供简单高效的应用数据存储与恢复能力

3.2. 对比Java序列化

MicroStream采用自定义序列化机制,专为替代传统DBMS而设计,性能更优。它摒弃了Java内置序列化,主要因为后者存在三大硬伤:

  • 只能存储和恢复完整对象图
  • 存储空间和性能效率低下
  • 类结构变更时需要手动处理

而MicroStream的自定义存储方案支持:

  • 按需持久化/加载/更新部分对象图
  • 高效处理存储空间和性能
  • 通过内部启发式算法或用户自定义映射策略自动处理类结构变更

4. 对象图存储实践

MicroStream通过单一数据结构和单一数据模型简化开发。对象实例以字节流存储,引用关系通过唯一标识符映射,实现对象图的快速存储和按需加载。

4.1. 依赖配置

使用前需添加两个核心依赖:

<dependency>
    <groupId>one.microstream</groupId>
    <artifactId>microstream-storage-embedded</artifactId>
    <version>07.00.00-MS-GA</version>
</dependency>
<dependency>
    <groupId>one.microstream</groupId>
    <artifactId>microstream-storage-embedded-configuration</artifactId>
    <version>07.00.00-MS-GA</version>
</dependency>

4.2. 根实例设计

整个数据库通过根实例访问,这是对象图的入口点。根实例可以是任意Java类型,甚至直接用String:

EmbeddedStorageManager storageManager = EmbeddedStorage.start(directory);
storageManager.setRoot("baeldung-demo");
storageManager.storeRoot();

但String根实例无法包含子对象,实际开发中通常需要自定义应用专属的根类型

public class RootInstance {

    private final String name;
    private final List<Book> books;

    public RootInstance(String name) {
        this.name = name;
        books = new ArrayList<>();
    }

    // 标准getter、hashCode和equals方法
}

注册自定义根实例的方式类似:

EmbeddedStorageManager storageManager = EmbeddedStorage.start(directory);
storageManager.setRoot(new RootInstance("baeldung-demo"));
storageManager.storeRoot();

初始books列表为空,但通过自定义根可后续存储Book实例:

RootInstance rootInstance = (RootInstance) storageManager.root();
assertThat(rootInstance.getName()).isEqualTo("baeldung-demo");
assertThat(rootInstance.getBooks()).isEmpty()
storageManager.shutdown();

操作完成后务必调用shutdown()确保数据安全

5. 数据操作指南

5.1. 存储操作

存储新对象时,必须对新对象的直接容器调用store()方法。在示例中即books列表:

RootInstance rootInstance = (RootInstance) storageManager.root();
List<Book> books = rootInstance.getBooks();
books.addAll(booksToStore);
storageManager.store(books);
assertThat(books).hasSize(2);

存储对象时会自动持久化其引用的所有对象,且store()调用保证数据已物理写入存储层(通常是文件系统)。

5.2. 急加载机制

急加载(Eager Loading)是默认的数据加载方式。启动时若发现已有数据库,整个对象图会被完整加载到内存

EmbeddedStorageManager storageManager = EmbeddedStorage.start(directory);
if (storageManager.root() == null) {
    RootInstance rootInstance = new RootInstance("baeldung-demo");
    storageManager.setRoot(rootInstance);
    storageManager.storeRoot();
} else {
    RootInstance rootInstance = (RootInstance) storageManager.root();
    // 使用从存储加载的现有根实例
}

根实例为null表示存储中无数据库。

5.3. 懒加载实现

处理大数据量时,启动时全量加载可能不现实。MicroStream支持通过Lazy包装器实现懒加载:

private final Lazy<List<Book>> books;

使用Reference()方法创建Lazy包装的ArrayList:

books = Lazy.Reference(new ArrayList<>());

获取实际对象需调用get()方法(类似WeakReference):

public List<Book> getBooks() {
    return Lazy.get(books);
}

get()调用会按需重新加载数据,开发者无需处理底层数据库标识符。

5.4. 删除操作

删除操作无需显式调用删除方法,只需清除对象图中的所有引用并存储变更:

List<Book> books = rootInstance.getBooks();
books.remove(1);
storageManager.store(books);

已删除数据不会立即从物理存储擦除,而是由后台维护进程定期清理

6. 查询系统

与标准DBMS不同,MicroStream查询直接在本地内存数据上执行,无需学习特殊查询语言,全部使用原生Java操作。常用方案是结合Stream API和集合:

List<Book> booksFrom1998 = rootInstance.getBooks().stream()
    .filter(book -> book.getYear() == 1998)
    .collect(Collectors.toList());

内存查询虽消耗内存但速度极快。存储和加载过程可通过多线程并行化,目前暂不支持水平扩展,但官方正在开发对象图复制方案,未来将支持集群和多节点数据复制。

7. 总结

本文深入探讨了MicroStream这个JVM对象图持久化引擎,展示了它如何通过统一内存操作和数据持久化的数据结构解决对象关系不匹配问题。我们实践了自定义根实例创建对象图,掌握了存储/删除/急加载/懒加载等操作,最后了解了基于内存操作的查询系统。

完整源码见GitHub仓库


原始标题:Guide to MicroStream