前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >嵌入式数据库 QuickIO 诞生记

嵌入式数据库 QuickIO 诞生记

原创
作者头像
artbits
发布2023-05-01 22:00:21
9970
发布2023-05-01 22:00:21
举报
文章被收录于专栏:梦想自习室梦想自习室

QuickIO 的诞生背景

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

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

代码语言:java
复制
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 存储文档数据的示例代码,来源于官方文档,有删改。

代码语言:c#
复制
// 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 存储文档数据的示例代码。

代码语言:java
复制
// 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 的基本概况

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

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

代码语言:java
复制
// 存储文档类型的数据
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毫秒左右。

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

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

代码语言:java
复制
// 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 的范围亦可以转换为相对应的时间戳范围。

代码语言:java
复制
// 查询 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 星球不完全指南》

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • QuickIO 的诞生背景
  • QuickIO 的灵感来源
  • QucikIO 与 LiteDB 的异同
  • QuickIO 的基本概况
  • 关于作者
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档