Node.js使用数据库LevelDB:超高性能kv存储引擎

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云原生数据库 PolarDB PostgreSQL 版,企业版 4核16GB
推荐场景:
HTAP混合负载
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
简介: Node.js被设计用来做快速高效的网络I/O。它的事件驱动流使其成为一种智能代理的理想选择,通常作为后端系统和前端之间的粘合剂。Node的设计初衷就是为了实现这一目的,但与此同时,它已成功用于构建传统的Web应用程序:一个HTTP服务器,提供为HTML页面或JSON消息响应,并使用数据库存储数据。

image.png

Node.js被设计用来做快速高效的网络I/O。它的事件驱动流使其成为一种智能代理的理想选择,通常作为后端系统和前端之间的粘合剂。Node的设计初衷就是为了实现这一目的,但与此同时,它已成功用于构建传统的Web应用程序:一个HTTP服务器,提供为HTML页面或JSON消息响应,并使用数据库存储数据。

尽管其他平台和语言有比较成熟的Web框架并更倾向于使用开源关系型数据库,如MySQL或PostgreSQL,大多数Node Web框架(如Express、Hapi等)并不强制使用任何特定的数据库,甚至根本不强制使用任何类型的数据库。昨天在《浅谈前端异常监控平台实现方案》一文中就提到LevelDB,今天跟大家介绍这个超高性能的Key-Value数据库LevelDB,同类型的比较流行的有MongoDB,本文暂不介绍MongoDB。

认识LevelDB

LevelDB是Google传奇工程师Jeff Dean和Sanjay Ghemawat开源的一款超高性能Key-Value存储引擎,以其惊人的读性能和更加惊人的写性能在轻量级NoSql数据库中鹤立鸡群,此开源项目目前是支持处理十亿级别规模Key-Value型数据持久性存储的C++ 程序库。在优秀的表现下对于内存的占用也非常小,大量数据都直接存储在磁盘上,可以理解为以空间换取时间。

设计思路

对于普通机械磁盘顺序写的性能要比随机写高很多,而LevelDB的设计思想正是利用了磁盘的这个特性。 LevelDB的数据是存储在磁盘上的,采用LSM-Tree的结构实现。LSM-Tree将磁盘的随机写转化为顺序写,从而大大提高了写速度。为了做到这一点LSM-Tree的思路是将索引树结构拆成一大一小两颗树,较小的一个常驻内存,较大的一个持久化到磁盘,共同维护一个有序的key空间。写入操作会首先操作内存中的树,随着内存中树的不断变大,会触发与磁盘中树的归并操作,而归并操作本身仅有顺序写。如下图所示:

image.png

上图为 LevelDB 整体架构,静态结构主要由六个部分组成:

  • MemTable(wTable):内存数据结构,具体实现是 SkipList。 接受用户的读写请求,新的数据修改会首先在这里写入。
  • Immutable MemTable(rTable):当 MemTable 的大小达到设定的阈值时,会变成 Immutable MemTable,只接受读操作,不再接受写操作,后续由后台线程 Flush 到磁盘上。
  • SST Files:Sorted String Table Files,磁盘数据存储文件。分为 Level0 到 LevelN 多层,每一层包含多个 SST 文件,文件内数据有序。Level0 直接由 Immutable Memtable Flush 得到,其它每一层的数据由上一层进行 Compaction 得到。
  • Manifest Files:Manifest 文件中记录 SST 文件在不同 Level 的分布,单个 SST 文件的最大、最小 key,以及其他一些 LevelDB 需要的元信息。由于 LevelDB 支持 snapshot,需要维护多版本,因此可能同时存在多个 Manifest 文件。
  • Current File:由于 Manifest 文件可能存在多个,Current 记录的是当前的 Manifest 文件名。
  • Log Files (WAL):用于防止 MemTable 丢数据的日志文件。

粗箭头表示写入数据的流动方向:

  1. 先写入 MemTable。
  2. MemTable 的大小达到设定阈值的时候,转换成 Immutable MemTable。
  3. Immutable Table 由后台线程异步 Flush 到磁盘上,成为 Level0 上的一个 sst 文件。
  4. 在某些条件下,会触发后台线程对 Level0 ~ LevelN 的文件进行 Compaction。
读操作的流动方向和写操作类似:
  1. 读 MemTable,如果存在,返回。
  2. 读 Immutable MemTable,如果存在,返回。
  3. 按顺序读 Level0 ~ Leveln,如果存在,返回。
  4. 返回不存在。

上面就是简单的介绍 LevelDB 的设计原理,架构和读写操作的数据流向,因为独特的设计原理,LevelDB很适合查询较少,写操作很多的场景。如果需要进一步了解其设计原理,就需要去学习跳跃表的设计算法,这里就不展开了。

跳跃表是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳跃表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳跃表的插入和删除的工作是比较简单的。

LevelDB特征:

  • key和value都是任意长度的字节数组;
  • 存储按键排序的数据;
  • 提供基本的put(key, value)get(key)delete(key)接口;
  • 支持批量操作以原子操作进行;
  • 可以创建数据全景的snapshot(快照),并允许在快照中查找数据;
  • 可以通过前向(或后向)迭代器遍历数据(迭代器会隐含的创建一个snapshot);
  • 自动使用Snappy压缩数据;
  • 可移植性;

基于以上特性,很多区块链项目都是采用LevelDB来作为数据存储。下面就开始介绍如何在项目中使用LevelDB,本文涉及的代码在项目Pretender-Service

安装LevelDB

项目是基于Node.js,基础环境就需要自己配置一下。执行一下命令:

npm install level --save

接下来还需要安装level-sublevel,为了方便更好的使用LevelDB,不用自己设计分键空间。

npm install level-sublevel --save

还需要为数据生成唯一ID的模块,这里我们安装 cuid

npm install cuid --save

安装完成后,将在应用程序中使用它,创建一个levelDb的类 ./src/db/levelDb.js

添加引用的依赖库

const level = require("level");
const sublevel = require("level-sublevel");

在构造函数中声明两个变量,代码如下:

class LevelDb {
    constructor(dbPath, options = {}) {
        this.options = options;
        this.db = sublevel(level(dbPath, { valueEncoding: "json" }));
    }
}
module.exports = LevelDb;

在这里,创建了一个导出LevelDB数据库的单例模块。首先需要level模块,并使用它来实例化数据库,并为其提供路径 dbPath 。此路径为 LevelDB 存储数据文件的目录路径。此目录专门用于 LevelDB ,可能在开始时不存在。在这里定义的路径为外部声明的时候传入进来。valueEncoding 定义了使用的值的格式为 json,支持一下格式:hexutf8base64binaryasciiutf16le

添加LevelDB操作方法

现在来为上面定义的类增加方法,常用的方法有 put、get、delete、batch、find。本文只是基本的操作,实际项目需要对数据进行合理的设计。

在构造函数中创建了LevelDB数据库对象this.db,可以调用其方法。

put

新增或者更新数据。

put(key, value, callback) {
        if (key && value) {
            this.db.put(key, value, (error) => {
                callback(error);
            });
        } else {
            callback("no key or value");
        }
    }

get

获取指定key的数据。

get(key, callback) {
        if (key) {
            this.db.get(key, (error, value) => {
                callback(error, value);
            });
        } else {
            callback("no key", key);
        }
    }

delete

删除指定key的数据

delete(key, callback) {
        if (key) {
            this.db.del(key, (error) => {
                callback(error);
            });
        } else {
            callback("no key");
        }
    }

batch

LeveLDB的一个强大功能是,它允许对要自动执行的批处理中的操作进行分组。

batch(arr, callback) {
        if (Array.isArray(arr)) {
            var batchList = [];
            arr.forEach(item);
            {
                var listMember = {};
                if (item.hasOwnProperty("type")) {
                    listMember.type = item.type;
                }
                if (item.hasOwnProperty("key")) {
                    listMember.key = item.key;
                }
                if (item.hasOwnProperty("value")) {
                    listMember.value = item.value;
                }
                if (
                    listMember.hasOwnProperty("type") &&
                    listMember.hasOwnProperty("key") &&
                    listMember.hasOwnProperty("value")
                ) {
                    batchList.push(listMember);
                }
            }
            if (batchList && batchList.length > 0) {
                this.db.batch(batchList, (error) => {
                    callback(error, batchList);
                });
            } else {
                callback("array Membre format error");
            }
        } else {
            callback("not array");
        }
    }

到这里已经完成一个 LevelDB 基础操作类,下面就开始将其应用于项目中。

使用LevelDB

接下来创建一个examples的文件夹,增加文件index.jskey-value数据库存储和关系型数据库有所不一样,关系型数据库每条记录一般都有一个唯一的自增长id,而key-value数据库需要自己维护一个类似id的唯一key值,这个key值的设计也影响数据查询是否遍历。这里我们只是做一个简单的实例,完整代码如下:

"use strict";
require("../src/utils/logger.js")(2);
const { dbConfig } = require("../config");
const LevelDB = require("../src/db/levelDb");
const path = require("path");
const cuid = require("cuid");
const dbHelper = new LevelDB(
    path.resolve(__dirname, dbConfig.path, dbConfig.folder)
);
// 增加用户信息
const administrators = [
    {
        name: "QuintionTang",
        email: "QuintionTang@gmail.com",
        password: "123456",
        id: "ckoyhjqbj0000mzkd1o63e31p",
    },
    {
        name: "JimGreen",
        email: "JimGreen@gmail.com",
        password: "123456",
        id: "ckoyhjqbk0001mzkdhuq9abo4",
    },
];
const keyPrefix = "administrator";
console.info("====>开始插入数据");
const administratorsKeys = [];
for (const item of administrators) {
    const uid = item.id;
    const keyName = `${keyPrefix}_${uid}`;
    item.id = uid;
    dbHelper.put(keyName, item, (error) => {
        if (error !== null) {
            administratorsKeys.push(keyName);
        }
    });
}
console.info("====>开始查找数据");
// 开始查找uid为 ckoyhjqbj0000mzkd1o63e31p 的用户信息
const findUid = "ckoyhjqbj0000mzkd1o63e31p";
const findKeyName = `${keyPrefix}_${findUid}`;
dbHelper.find(
    {
        prefix: findKeyName,
    },
    (error, result) => {
        console.info(result);
    }
);

接下来执行实例代码:

cd examples
node index.js

执行结果如下:

image.png

此时查看根目录下 database 就会生成数据库文件。

总结

本文简单介绍了LevelDB的设计原理,在项目中完成LevelDB类的定义,并实现一个简单的数据插入和查询。后续会在项目Pretender-Service完成更加复杂的业务逻辑,实现基本的CURD,敬请关注项目动态。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
2月前
|
数据库 JavaScript SQL
❤Nodejs 第八章(操作本地数据库优化查询为分页查询方式)
【4月更文挑战第8天】在Node.js中,本章讲述了如何优化本地数据库查询以实现分页。首先,添加了前端分页参数`pageNum`(页码)和`pageSize`(每页条数)。接着,通过打印`req.query`来验证参数是否正确传递。初始查询示例为`SELECT * FROM user WHERE age = 18 LIMIT 0, 10`。当改变分页参数时,查询能相应更新。在实现动态偏移量`offset`时,起初因误添加`' AND' : ' WHERE'`导致错误,修正后使用`LIMIT`和计算出的`offset`进行分页。
74 5
❤Nodejs 第八章(操作本地数据库优化查询为分页查询方式)
|
8天前
|
存储 关系型数据库 MySQL
|
4天前
|
XML 缓存 JavaScript
一篇文章讲明白JS模板引擎之JST模板
一篇文章讲明白JS模板引擎之JST模板
|
12天前
|
存储 关系型数据库 MySQL
MySQL数据库进阶第一篇(存储引擎与Linux系统上安装MySQL数据库)
MySQL数据库进阶第一篇(存储引擎与Linux系统上安装MySQL数据库)
|
12天前
|
存储 缓存 NoSQL
Redis是一种高性能的内存数据库,常用于高并发环境下的缓存解决方案
【6月更文挑战第18天】**Redis摘要:** 高性能内存数据库,擅长高并发缓存。数据存内存,访问迅速;支持字符串、列表等多元数据类型;具备持久化防止数据丢失;丰富命令集便于操作;通过节点集群实现数据分片与负载均衡,增强可用性和扩展性。理想的缓存解决方案。
26 1
|
23天前
|
存储 NoSQL 数据处理
探索MongoDB:灵活、高性能的NoSQL数据库解决方案与应用实践
探索MongoDB:灵活、高性能的NoSQL数据库解决方案与应用实践
106 1
|
24天前
|
存储 安全 关系型数据库
|
24天前
|
存储 关系型数据库 MySQL
MySQL数据库——存储引擎(2)-存储引擎特点(InnoDB、MyISAM、Memory)、存储引擎选择
MySQL数据库——存储引擎(2)-存储引擎特点(InnoDB、MyISAM、Memory)、存储引擎选择
19 1
|
24天前
|
缓存 自然语言处理 前端开发
深入剖析JavaScript引擎的工作原理
【6月更文挑战第3天】JavaScript引擎由解析器、解释器、优化器和垃圾回收器组成,它们协同完成代码的解析、编译和执行。解析器将源代码转为抽象语法树(AST),编译阶段进行作用域分析和变量提升。解释器执行AST,优化器在代码频繁执行时进行即时编译以提高性能。垃圾回收器自动回收不再使用的内存,防止泄漏。理解这些原理有助于优化代码和提升Web应用性能。
17 1
|
24天前
|
JavaScript NoSQL 数据库连接
使用Nodejs + express连接数据库mongoose
【6月更文挑战第3天】这篇文章介绍了如何在Express应用中使用Mongoose连接MongoDB数据库。首先,需要创建一个`db.js`文件,然后通过`npm install mongoose`安装Mongoose驱动。接着,在应用中引入MongoDB模块,建立到数据库的连接。创建一个Mongoose schema定义数据模型,如用户信息表。最后,执行数据库操作,包括查询、插入、更新和删除文档,并在完成后关闭数据库连接。文中还提供了相关代码示例。
15 1

热门文章

最新文章