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.ecomenagepro.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]

發表評論