TAT.李强 Node 嵌入式数据库——NeDB
In 未分类 on 2016年03月30日 by view: 46,258
7

一、简介

NeDB 是使用 Nodejs 实现的一个 NoSQL 嵌入式数据库操作模块,可以充当内存数据库,也可以用来实现本地存储,甚至可以在浏览器中使用。查询方式比较灵活,支持使用正则、比较运算符、逻辑运算符、索引以及 JSON 深度查询等。

NeDB 嵌入到了应用程序进程中,消除了与客户机服务器配置相关的开销,在运行时,也只需要较少的内存开销,使用精简代码编写,速度更快。其 API 是 MongoDB 的一个子集,可以通过这些接口轻松管理应用程序数据,而不依靠原始的文档文件。

具有简单、轻量、速度快等特点,由于嵌入式数据库存储总数据量最好要控制在 1GB 以内,所以适合在不需要大量数据处理的应用系统中使用(比如使用 nw.js 等实现的桌面应用程序、并发量不大的系统等)。

二、Git 地址

https://github.com/louischatriot/nedb

三、快速上手

由于 NeDB 可以看作是精简版的 MongoDB,这里和 MongoDB 的使用做一下对比,以便可以更直观的感受 NeDB 的简便。

MongoDB

1、下载安装包;(http://www.mongodb.org/downloads

2、解压缩文件;

3、设置系统变量;

4、配置 mongodb 运行环境;

5、启动 mongodb 服务;

6、连接 mongodb;

7、添加 mongodb 为 windows 服务;

8、启动服务;

9、安装 mongoose 模块(mongoose 官网 http://mongoosejs.com/

10、使用(以 express 为例)

11、停止或删除服务;

NeDB

1、安装模块

2、使用

通过对比,嵌入式数据库在使用上的优势一目了然,无需任何数据库服务器,也不用安装、配置、启动一个数据库服务,而且 NeDB 的 API 抽取了 MongoDB 常用的一些接口,在用法上大同小异,性能也不错。如果项目使用 Node 实现,并且存储数据量不大,又熟悉 MongoDB 语法,那么,NeDB 就值得一用。详细用法请参照官方文档或下方中文翻译文档。

注:对于习惯了关系型数据库的开发人员来说,有些术语以及坑需要重申一下:

1、“ 表” 对应“ 集合 (collection)”,“ 行” 对应“ 文档(document)”,一个 database 中可以有多个 collection,一个 collection 中又可以有多个 document;

2、NeDB 默认 utf-8 编码;

3、严格区分大小写,比如查询 db.find({"name":"tom"}) 和 db.find({"Name":"tom"}) 并不是用的同一字段做的条件;

如果您在使用过程中遇到其他问题,可以留言,我们一起补充。

四、选择

之所以写该节,是因为本文介绍 NeDB,但并不是推荐 NeDB。选取什么样的数据库主要取决于项目以及个人情感。由于涉及到 SQL 数据库与 NoSQL 数据库的概念,所以先从大的方面简单说一下,然后再介绍 Node 嵌入式数据库。

先简单回顾下数据库的分类。

数据库通常分为层次式数据库、网络式数据库和关系式数据库三种。而不同的数据库是按不同的数据结构来联系和组织的。在当今的互联网中,最常见的数据库模型主要有两种:关系型数据库和非关系型数据库。

关系型数据库

关系型数据库模型是把复杂的数据结构归结为简单的二元关系(即二维表格形式)。在关系型数据库中,对数据的操作几乎全部建立在一个或多个关系表格上,通过对这些关联的表格分类、合并、连接或选取等运算来实现数据库的管理。主流关系型数据库有 Oracle、MySQL、MariaDB、SqlServer、Access、PostgreSQL、DB2 等。其中个人感觉 PostgreSQL 功能十分强大,虽是关系型数据库,但支持 json 和 Hstore 字段,兼有事务和文档特性,只是性能就差了点。

非关系型数据库

NoSQL 意味着 Not only SQL。面对超大规模和高并发的 SNS(社交网络服务) 类型的 web2.0 纯动态网站,传统的关系型数据库显得有些力不从心,比如表的横向扩展等。NoSQL 数据库作为传统关系型数据库的有效补充,在特定的场景下可以发挥出难以想象的高效率和高性能。主流的非关系型数据库分为键值存储数据库 (Memcached、Redis 等),列存储数据库 (HBase 等),图形数据库 (Neo4j 等),面向文档数据库 (MongoDB、CouchDB 等)。

由于 NeDB 属于面向文档数据库,这里提及一下该类数据库,了解其它类型数据库可以自行查询官方文档。面向文档数据库可以看做是键值数据库的一个升级,不但允许键值嵌套,还提高了查询效率。面向文档数据库会将数据以文档形式存储。每个文档都是自包含的数据单元,是一系列数据项的集合。每个数据项都有一个名词与对应值,值既可以是简单的数据类型,如字符串、数字和日期等;也可以是复杂的类型,如有序列表和关联对象。数据存储的最小单位是文档,同一个表中存储的文档属性可以是不同的,数据可以使用 XML、JSON 或 JSONB 等多种形式存储。

介绍完分类,接下来就简单说一下各自的使用场景。

RDBMS

特点:

  • 提供事务,使两个或两个以上的成功或失败的数据更改作为一个原子单元;
  • 高度组织化结构化数据;
  • 数据和关系都存储在单独的表中;
  • 需要预先定义表模式;
  • 鼓励标准化减少数据冗余;
  • 支持多表查询;
  • 强制数据完整性;
  • 严格的一致性;
  • 支持扩展(横向扩展有些痛苦);
  • 结构化查询语言(SQL);
  • 诞生 40 年之多,十分成熟,有足够的支持;

从其特点分析,可看出其适合有明确的定义,规范比较明确的项目。比如在线商城和银行系统等。该类系统需要具备强制数据完整性以及事务支持的健壮存储系统。可以试想一下如果去 ATM 机取钱,ATM 机没有吐钱,但是后台数据库已经把钱减掉了,会是一种什么样的体验呢?

NoSQL

特点:

  • Not only SQL;
  • 没有声明性查询语言;
  • 没有预定义的模式;
  • 键-值对存储,列存储,文档存储,图形数据库;
  • 最终一致性,而非 ACID 属性;
  • 非结构化和不可预知的数据;
  • CAP 定理 ;
  • 高性能,高可用性和可伸缩性;
  • 是一个新的、令人兴奋的技术,并不是十分成熟;

从其特点分析,最适合无固定要求的组织数据。比如社交网络、客户管理和网络监控系统等。

就客户管理系统来说,假如刚开始使用关系型数据库建一个联系人的表,表字段有主键 id、姓名 name、电话 telephone、邮箱 email、地址 address。那么问题来了,现在有联系人有三个电话号码(住宅座机、移动电话、工作电话)需要输入,这时就要考虑单独创建一个 telephone 表,这样就不受限制了,也让我们的数据标准化了。新建 telephone 表结构:联系人 contact_id、号码类型 name、号码 num。email 与 address 也存在同样的问题,address 的情况更加复杂,这里不再展开。对关系型数据库来说,Schema 是固定不变的,而我们事先是不能预测所有字段的,比如刚才的联系人表,很快我们会发现当前字段不能满足,比如要添加性别 gender、年龄 age、生日 birthday 等字段,那么最后就导致需要加一个 otherdata 表。数据又是碎片化的,当查询一个联系人时,如果该联系人有 3 个电话号码、2 个 email 地址和 5 个地址,那么 SQL 查询需要检查所有表,并将产生 3*2*5=30 条结果,使得全文搜索很困难。

面对这种情况,如果选择 NoSQL 数据库,联系人列表将从中受益。数据库将一个联系人的所有数据存储在一个单独的文档里的 contacts 集合里。

如果这时需要添加一些数据,这些数据没有必要应用到之前的联系人,NoSQL 数据库就可以随意添加或移除字段。联系人数据存储在单独的文档中,也使得全文搜索变得简单。

介绍完 SQL 与 NoSQL 数据库的基本概念,就该回到正题啦,介绍下 Node 嵌入式数据库,SQLite 同样也有 SQL 与 NoSQL 之分。

Nodejs 可用的 SQLite 有 node-sqlite,node-sqlite3,NeDB,nStore 以及 final-db 等。其中 node-sqlite,node-sqlite3 属于 SQL 数据库,NeDB,nStore 以及 final-db 属于 NoSQL 数据库。如需详细了解各个模块,可以去看相应的官方文档。

其中使用最多的应该算 node-sqlite3 和 NeDB 了,两者的区别很明显,前者是 SQL 数据库,后者是 NoSQL 数据库。另外,sqlite3 相对 NeDB 要重一些,性能也要差一点,使用 SQL 语句失去了 js 直接操作 json 的简便,API 也相对复杂很多。而 NeDB 只提供了基本的 CURD 操作,只能用于小型应用,大场景并不适用,数据加载到内存中进行操作,不适合内存非常紧张的应用,目前作者也没有给出具体的内存控制方法。。

五、源码简析

想要更深入的理解 NeDB,就需要了解它是如何实现的。我这里给出一些我阅读源码时记忆比较深刻的几个点。

1、所有改变数据的操作 (indert,update,remove),都会触发 persistence.persistNewState 方法,比如可以看一下 datastore.js 第 265-268 行的 Datastore.prototype._insert 方法。该方法决定数据的去处,如果是当作内存数据库来用,该方法会提前返回,如果是本地文档持久化存储,则会将数据经过 utf-8 编码序列化之后追加到备份数据库的文档中。

2、数据在 model.js 中通过 serialize 方法被序列化,该方法使用 JSON.stringfy 序列化 json 数据,在回调函数中将 undefined 类型值映射为 null,并且使用与 mongoDB 相同的规则 (不能以"$" 开头,也不能包含".") 校验属性的有效性。

3、数据从硬盘上加载到内存时,使用了 async 模块的 waterfall 方法。该方法参数是由方法组成的数组,并且先执行的方法会将执行结果传入下一个方法,方法按顺序执行,并且当其中一个方法报错,就会导致后面的方法不再执行,直接在主方法回调抛出异常。

4、包括当持久层初始化时从磁盘上加载数据在内的所有的操作命令都会通过 Executor 类的实例,将方法传入队列,保证命令可以按序执行(包括同步与异步方法)。

源码并不难理解,通过以上几点,希望可以让大家更容易解读源码。通读 NeDB 的源码,对 Node 异步 I/O 以及基于事件编程的思想会有进一步的认识。

为了方便大家理解,对官方文档做了简单翻译,如有不当的地方希望大家指正,中文 API 文档如下:

六、API

1、new Datastore(options)

作用:

初始化一个数据存储,相当于 MongoDB 的一个集合、Mysql 的一张表。

options 对象配置参数:

① filename(可选): 数据存储文件路径。如果为空,数据将会自动存储在内存中。注意路径不能以“~” 结尾。

② inMemoryOnly(可选, 默认 false): 数据存储方式。是否只存在于内存中。

③ loadDatabase: 将数据加载到内存中。

④ timestampData(可选, 默认 false): 自动生成时间戳,字段为 createdAt 和 updateAt,用来记录文档插入和更新操作的时间点。

⑤ autoload(可选, 默认 false): 如果使用 autoload,当数据存储被创建时,数据将自动从文件中加载到内存,不必去调用 loadDatabase。注意所有命令操作只有在数据加载完成后才会被执行。

⑥ onload(可选): 在数据加载完成后被调用,也就是在 loadDatabase 方法调用后触发。该方法有一个 error 参数,如果试用了 autoload,而且没有定义该方法,在数据加载过程中出错将默认会抛出该错误。

⑦ afterSerialization(可选): 在数据被序列化成字符串之后和被写入磁盘前,可以使用该方法对数据进行转换。比如可以做一些数据加密工作。该方法入参为一个字符串 (绝对不能含有字符“\n”,否则数据会丢失),返回转换后的字符串。

⑧ beforeDeserialization(可选): 与 afterSerialization 相反。两个必须成对出现,否则会引起数据丢失,可以理解为一个加密解密的过程。

⑨ corruptAlertThreshold(可选): 默认 10%, 取值在 0-1 之间。如果数据文件损坏率超过这个百分比,NeDB 将不会启动。取 0,意味着不能容忍任何数据损坏;取 1,意味着忽略数据损坏问题。

⑩ compareStrings(可选): compareStrings(a, b) 比较两个字符串,返回-1、0 或者 1。如果被定义,将会覆盖默认的字符串比较方法,用来兼容默认方法不能比较非 US 字符的缺点。

注:如果使用本地存储,而且没有配置 autoload 参数,需要手动调用 loadDatabase 方法,所有操作 (insert, find, update, remove) 在该方法被调用前都不会执行。还有就是,如果 loadDatabase 失败,所有命令也将不会执行。

示例

2、db.insert(doc, callback)

作用:

插入文档数据 (文档相当于 mysql 表中的一条记录)。如果文档不包含_id 字段,NeDB 会自动生成一个,该字段是 16 个字符长度的数字字符串。该字段一旦确定,就不能被更改。

参数:

doc: 支持 String, Number, Boolean, Date, null, array 以及 object 类型。如果该字段是 undefined 类型,将不会被保存,这里和 MongoDB 处理方式有点不同,MongoDB 会将 undefined 转换为 null 进行存储。字段名称不能以"$" 开始,也不能包含"."。

callback(可选): 回调函数,包含参数 err 以及 newDoc,err 是报错,newDoc 是新插入的文档,包含它的_id 字段。

示例

3、db.find(query, callback)

作用:

查询符合条件的文档集。

参数:

query: object 类型,查询条件。支持使用比较运算符 ($lt, $lte, $gt, $gte, $in, $nin, $ne), 逻辑运算符 ($or, $and, $not, $where), 正则表达式进行查询。

callback(可选): 回调函数,包含参数 err 以及 docs,err 是报错,docs 是查询到的文档集。

示例:

4、db.findOne(query, callback)

作用:

查询符合条件的一个文档。与 db.find 使用相同。

5、db.update(query, update, options, callback)

作用:

根据 update 参数的规则,更新匹配到 query 的结果集。

参数:

query: 与 find 和 findOne 中 query 参数的用法一致

update: 指定文档更改规则。该参数可以是一个新的文档,也可以是一套修饰符,两者不能同时使用。使用修饰符时,如果需要更改的字段不存在,将会自动创建。可用的修饰符有 $set(改变字段值), $unset(删除某一字段), $inc(增加某一字段), $min/$max(改变字段值,传入值需要小于/大于当前值), 还有一些用在数组上的修饰符,$push, $pop, $addTopSet, $pull, $each, $slice,具体用法如下示例。

options: object 类型。muti(默认 false),是否允许修改多条文档;upsert(默认为 false),如果 query 没有匹配到结果集,有两种情况需要考虑,一个是 update 是一个简单的对象 (不包含任何修饰符),另一种情况是带有修饰符,对第一种情况会直接将该文档插入,对第二种情况会将通过修饰符更改后的文档插入;

callback(可选): 参数 (err, numAffected, affectedDocuments, upsert)。numAffected:被影响的文档个数;affectedDocuments:更新后的文档。

注意:_id 不能被修改

示例:

6、db.remove(query, options, callback)

作用:

根据 options 配置删除所有 query 匹配到的文档集。

参数:

query: 与 find 和 findOne 中 query 参数的用法一致

options: 只有一个可用。muti(默认 false),允许删除多个文档。

callback: 可选,参数: err, numRemoved

示例:

7、db.ensureIndex(options, callback)

作用:

NeDB 支持索引。索引可以提高查询速度以及保证字段的唯一性。索引可以用在任何字段,包括嵌套很深的字段。目前,索引只能用来加速基本查询以及使用 $in, $lt, $lte, $gt 和 $gte 运算符的查询,如上 find 接口中示例所示。保证索引不为数组对象。方法可以在任何时候被调用,推荐在应用启动时就调用 (该方法是同步的, 为 1000 个文档添加索引仅需 35ms)。

参数:

fieldName(必须): 索引字段,使用“.” 给嵌套的字段加索引。

unique(可选,默认 false): 字段唯一性约束。注意:唯一性约束会增加为两个文档中没有定义的字段添加索引的错误。

sparse(可选,默认 false): 不能为没有定义的字段加索引。如果接受给多个文档中没有定义的字段添加索引,给需要该配置参数与 unique 一起使用。

expireAfterSeconds(可选,秒数): TTL 索引,设置自动过期时间。

删除索引: db.removeIndex(fieldName, cb)

注意:_id 字段会自动加索引和唯一性约束,不必再为它使用 ensureIndex。如果使用本地存储,索引也将保存在数据文件中,当第二次加载数据库时,索引也将自动被添加。如果加载一个已经有索引的数据库,删除索引将不起任何作用。

8、db.count(query, callback)

作用:

计数。与 find 用法相同。

示例:

9、db.persistence.compactDatafile

作用:

为了性能考虑,NeDB 存储使用 append-only 格式,意味着所有的更改和删除操作其实都是被添加到了文件末尾。每次加载数据库时,数据库会自动被压缩,才能拿到规范的文档集。

也可以手动调用压缩方法 db.persistence.compactDatafile(该方法没有参数)。函数内部有队列机制,保证命令按顺序执行。执行完成后,会触发 compaction.done 事件。

也可以设置自动压缩方法 db.persistence.setAutocompactionInterval(interval) 来定时执行。interval 是毫秒级别 (大于 5000ms)。停止自动压缩使用方法 db.persistence.stopAutocompaction()。

压缩会花费一些时间 (在普通机器上,5w 条记录花费 130ms 处理,并不会耗费太久)。在压缩执行期间,其他操作将不能执行,所以大部分项目不需要使用它。

假设不受 corruptAlertThreshold 参数的限制,压缩将会把损坏的记录全部移除掉。

压缩会强制系统将数据写入磁盘,这就保证了服务崩溃不会引起数据的全部丢失。最坏的情况就是崩溃发生在两个压缩同步操作之间,会导致全部数据的丢失。

性能

在普通机器上,对于 1 万条记录

NeDB 吞吐量 (带索引):

Insert: 5950 ops

Find: 25440 ops

Update: 4490 ops

Remove: 6620 ops

原创文章转载请注明:

转载自AlloyTeam:http://www.alloyteam.com/2016/03/node-embedded-database-nedb/

  1. Terry 2016 年 11 月 30 日

    用 NeDB 现在最蛋碎的问题是所有数据操作都是异步的,查询无所谓,进行添加,修改,删除这些操作,通常需要按照可控的顺序执行或者会被封装成 Function 供调用,异步的形式就比较麻烦了

    • yee 2018 年 4 月 23 日

      Great

  2. simon3000 2016 年 5 月 24 日

    node 还有一个个人感觉不错但是功能有点少的数据库叫 level 不知道和这个 NeDB 比怎么样那个好像速度快一点不过功能少了

    • simon3000 2016 年 5 月 24 日

      我还用过一个用 lodash 控制的数据库叫做 lowdb 不过他那个是 json 储存所以数据库一大就会消耗很多内存并且很慢但是他 APi 都是同步的所以数据少倒是可以当很简单的数据库用

    • XFEPeter 2019 年 6 月 11 日

      level 可扩展性非常强,只是同样存在瓶颈

  3. 美图共赏 2016 年 4 月 14 日

    美图在这里:http://www.fydzv.com

  4. TAT.Johnny

    碧青 2016 年 4 月 1 日

    细细读完,好文点赞 [good]

发表评论