美文网首页Android开发Java
嵌入式数据库 QuickIO 诞生记

嵌入式数据库 QuickIO 诞生记

作者: 1225fe6bdfc1 | 来源:发表于2023-05-02 14:31 被阅读0次

QuickIO 的诞生背景

一年前,我在业余时间编写一个后端项目,项目使用的技术栈是 Java Vert.x + MongoDB。Vert.x 是一个事件驱动的网络应用程序框架,因其异步响应的特性,读写 MongoDB 时不可避免要编写大量异步回调的代码。“回调地狱”现象的产生,让代码的可读性逐渐下降。

Vert.x MongoDB Client 相关代码示例:

JsonObject document = new JsonObject().put("title", "The Hobbit");
mongoClient.save("books", document, res -> {
    if (res.succeeded()) {
        System.out.println("Saved book with id " +  res.result());
    } else {
        res.cause().printStackTrace();
    }
});

面对使用 MongoDB 需要编写大量异步代码的问题,当时又考虑到项目存储的数据量较小,或许可以使用嵌入式的 SQLite 代替 MongoDB,从而减少项目异步代码的编写。但选择 SQLite 这种关系型数据库还不是理想方案,因为项目存储的数据是非结构化的,所以使用像 MongoDB 这种非关系型数据库更为合适。因此,我需要寻找一个嵌入式 NoSQL 数据库。

QuickIO 的灵感来源

我带着问题 Google 一下,结果意外搜索到 C# 领域存在一个嵌入式 NoSQL 数据库 —— LiteDB , 其设计灵感来自 MongoDB,它的 API 与官方的 MongoDB .NET API 非常相似。然后我又搜索 Java 领域是否存在类似的数据库,很遗憾!没找到。因此,我萌发了编写一个 Java 嵌入式 NoSQL 数据库的念头。

LiteDB 的 LINQ 语法,用 Lambda 表达式即可完成数据库的增删改查,代码表现得十分优雅。这个特点成为我设计 QuickIO 时的一个明确要借鉴的方向。接着,确定数据库的引擎使用 LevelDB, 数据的序列化和反序列化使用 Hessian,后期为了提升数据库性能,使用 Protostaff 替换了 Hessian。

后来,该项目开源到 GitHub,经过频繁的迭代,编写的嵌入式 NoSQL 数据库逐渐成型。不久前,我初次发表了《一个轻量级Java嵌入式数据库——QuickIO》一文,简单介绍了 QuickIO 这一项目。

开源地址:https://github.com/artbits/quickio

QucikIO 与 LiteDB 的异同

前面提到创作 QuickIO 的灵感源于 LiteDB , 现在展示一下 C# 的 LiteDB 和 Java 的 QuickIO 在读写数据时,编写代码风格的异同,了解其是如何借鉴和参考的。

Talk is cheap. Show me the code. —— Linus Torvalds
使用 C# 的 LiteDB 存储文档数据的示例代码,来源于官方文档,有删改。

// Create your POCO class entity
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string[] Phones { get; set; }
    public bool IsActive { get; set; }
}

// Open database (or create if doesn't exist)
using(var db = new LiteDatabase(@"C:\Temp\MyData.db"))
{
    // Get a collection (or create, if doesn't exist)
    var col = db.GetCollection<User>("Users");

    // Create your new user instance
    var user = new User
    { 
        Name = "John Doe", 
        Phones = new string[] { "8000-0000", "9000-0000" }, 
        IsActive = true
    };
    
    // Insert new user document (Id will be auto-incremented)
    col.Insert(user);
    
    // Update a document inside a collection
    user.Name = "Jane Doe";
    
    col.Update(user);
    
    // Use LINQ to query documents
    var results = col.Query()
        .Where(x => x.Name.StartsWith("J"))
        .Limit(10)
        .ToList();

    // and now we can query phones
    var r = col.FindOne(x => x.Phones.Contains("8888-5555"));
}

使用 Java 的 QuickIO 存储文档数据的示例代码。

// Create your POCO class entity
public class User extends IOEntity {
    public String name;
    public String[] phones;
    public Boolean isActive;

    public static User of(Consumer<User> customer) {
        User user = new User();
        customer.accept(user);
        return user;
    }
}

// Open database (or create if doesn't exist)
try (DB db = QuickIO.usingDB("MyData")) {
    // Get a collection
    Collection<User> col = db.collection(User.class);

    // Create your new user instance
    User user = User.of(u -> {
        u.name = "John Doe";
        u.phones = new String[]{"8000-0000", "9000-0000"};
        u.isActive = true;
    });

    // Insert new user document (_id will be auto-incremented)
    col.save(user);

    // Update a document inside a collection
    user.name = "Jane Doe";

    col.save(user);

    // Use Java lambda to query documents
    List<User> users = col.find(x -> x.name.startsWith("J"), options -> options.limit(10));

    // and now we can query phones
    User u = col.findOne(x -> Arrays.asList(x.phones).contains("8888-5555"));
}

通过上述示例代码的对比,两个数据库在查询数据时,并没有使用到 SQL 或 BSON 语句。LiteDB 通过 C# 的语言特性 LINQ 完成数据查询,因为 Java 不具备这一语言特性(表达式树),所以 QuickIO 只是使用 Lambda 表达式模拟出类似 LiteDB 的 API 风格,并且 QuickIO API 风格也有别于一些 Java ORM API 风格。综上所述,使用 QuickIO 进行数据的增删改查,类似于 Java Stream 流的操作。

QuickIO 的基本概况

使用场景有哪些?可用于客户端程序的数据存储,服务端小微型程序的数据存储,单机或嵌入式程序的数据存储,更多的使用场景还有待探索。

支持存储那些类型的数据?支持存储文档、键值对、文件类型的数据。示例代码如下:

// 存储文档类型的数据
db.collection(Book.class).save(Book.of(b -> {
    b.name = "On java 8";
    b.author = "Bruce Eckel";
    b.price = 129.8;
}));

// 存储键值对类型的数据
kv.write("Pi", 3.14);
kv.write(3.14, "Pi");
double d = kv.read("Pi", Double.class);
String s = kv.read(3.14, String.class);

// 存储文件类型的数据
tin.put("photo.png", new File("..."));
File file = tin.get("photo.png");

如何对每种类型的数据进行存储?文档和键值对类型的数据存储主要依靠 LevelDB + Protostaff 完成。因为 LevelDB 是 KV 数据库引擎,每条数据以key : value的格式进行存储,所以 QuickIO 使用 Snowflake 算法生成唯一 ID 作为 key,Java 对象作为 value,key 和 value 通过 Protostaff 序列化后存入 LevelDB 中,而读取数据只是上述过程的反向操作。对于文件类型的数据的存储,则是在 Java NIO 的基础上进行操作。

为何选择 LevelDB & Protostaff ?LevelDB 作为 KV 数据库引擎,其性能较为优越,提供的 API 相对简单,Java 平台的 LevelDB 库相对于 RocksDB 库的大小更小,完全满足编写嵌入式 NoSQL 数据库的需要。Protostaff 是一种 Protobuf 协议的序列化工具,而 Protobuf 是一个灵活的、高效的用于序列化数据的协议,因此,使用 Protostaff 可以提高数据序列化的效率,这点可以参考开源项目 MMKV。

QuickIO 如何实现类似 LiteDB 的 API? LevelDB 是以键值的方式存储数据,面对条件查询,QuickIO 通过遍历数据的方式进行查询,拿出每条数据进行比对,筛选出满足条件的数据。选择遍历的方式进行数据查询,是基于对 LevelDB 顺序读的性能优越的肯定,同时,也对反序列化数据的过程进行了优化,提升遍历的速度。一般情况下,条件查询,遍历10w条数据,耗时700毫秒左右。

// 查询价格大于或等于100的书籍的数据,降序排序,跳过前5条数据,限制返回10条数据
List<Book> books = collection.find(b -> b.price >= 100, options -> options.sort("price", -1).skip(5).limit(10));

如何实现索引的支持?LevelDB 自身是不支持索引的,当需要从大量的数据中查找其中一条,若只靠遍历数据的方式查询,随着数据规模的增长,迟早会力不从心。因此,QuickIO 实现了索引功能,该功能也是基于 LevelDB 设计,但只是实现了唯一索引。通过索引查询数据,速度也实现了质的飞跃。

// Book 的实体类的字段 isbn 为索引字段,实现索引查询
Book book = collection.findWithIndex(options -> options.index("isbn", "9787115585011"));

为何选择 Snowflake ID 作为 key?使用 Snowflake ID 作为 LevelDB 的 key 时,当条件查询为 id 或 createdAt 时,QuickIO 无需反序列化 LevelDB 的 value,即可完成数据的初步筛选,从而提升查询效率。同时,Snowflake ID 的范围亦可以转换为相对应的时间戳范围。

// 查询 id 比 minId 大的书籍的数据。
List<Book> books = collection.findWithID(id -> id > minId);
// 查询创建时间戳比当前时间戳小的书籍的数据。
List<Book> books = collection.findWithTime(createdAt -> createdAt < System.currentTimeMillis());

QucikIO 早期版本代码较为简单,随着不断迭代,代码和内部设计也逐渐变得复杂,因本文篇幅有限,无法一一详细探讨。关于更多的详细内容,后续我有空闲时间,再撰文分享,计划先后通过多章节详细介绍其的使用方法和内部实现。

关于作者

关于学习经历,计算机网络工程专业,因兴趣爱好而学习编程。关于工作经历,一直就职于非技术的产品岗位,不具有技术岗位的从业经验。

对于数据库的开发,作者并无相关经验,一切都是业余时间从零开始学习和探索。在编写数据库的过程中,也学习了解到一些优秀的数据库项目,例如 MongoDB、SQLite、MMKV、TiDB、LiteDB、NeDB、PoloDB 等。其中,TiDB 官方分享的文章更是深入浅出且循序渐进。TiDB 是一个分布式数据库,其底层使用到 RocksDB,而 RocksDB 又是在 LevelDB 的基础上开发的。所以 TiDB 分享的文章,对我来说具有很大的学习价值,若大家也感兴趣,推荐阅读:《TiDB 星球不完全指南》

因作者并非相关领域的专业人士,技术水平有限,若本文存在错误的内容,又或编写的数据库项目存在错误的设计,恳请大家批评指正。

相关文章

  • 3. Spring Boot SQL Databases

    说明 嵌入式数据库使用内存中的嵌入式数据库开发应用程序通常很方便。显然,内存数据库不提供持久存储。您需要在应用程序...

  • 数据库之SQLite(平凡之路)

    什么是SQLite?数据库存储数据的步骤 ●SQLite是一款轻型的嵌入式数据库,它占用资源非常的低,在嵌入式设备...

  • Neo4j简介与安装

    官方网站git项目地址中文文档NoSql图形数据库,嵌入式、高性能、轻量级,面向网络的数据库,它是一个嵌入式的、基...

  • h2数据库

    1. java 操作 嵌入式连接 远程连接 内存数据库

  • sailsjs - 学习笔记

    控制器 嵌入式策略 定制路由 api 服务 数据库连接(mongoDB) 部署 监视 控制器 嵌入式策略 定制路由...

  • 嵌入式数据库的使用

    1. 简介: 我们在开发环境中经常需要用到嵌入式数据库来处理测试等相关的处理,嵌入式数据库一般会跟自身应用程序工作...

  • 嵌入式系统开发中常用的数据库

    数据库是一种储存和管理、组织数据的仓库,在嵌入式开发当中起到至关重要的作用。一个在嵌入式中,我们可学习使用的数据库...

  • Day03

    Android下的数据库 Sqlite数据库, 移动平台轻量级嵌入式的数据库,一般用于IOS,Android等移动...

  • Go的K/V数据库BoltDB使用教程

    如果你想让你的Go应用中的数据持久化,大多数人会使用一些数据库。最简单最方便的选择是嵌入式数据库,有很多嵌入式数据...

  • SQLite语句以及FMDB基本使用方法

    SQLite 什么是SQLite SQLite是一款轻型的嵌入式数据库它占用资源非常的低,在嵌入式设备中,可能只需...

网友评论

    本文标题:嵌入式数据库 QuickIO 诞生记

    本文链接:https://www.haomeiwen.com/subject/qvgajdtx.html