最近小組準備啟動一個 node 開源項目,從前端親和力、大資料下的IO性能、可擴充性幾點入手挑選了 NoSql 資料庫,但具體使用哪一款産品還需要做一次選型。
我們最終把選項範圍縮窄在 HBase、Redis、MongoDB、Couchbase、LevelDB 五款較主流的資料庫産品中,本文将主要對它們進行分析對比。
鑒于缺乏項目中的實戰經驗沉澱,本文内容和觀點主要還是從各平台資料搜羅彙總,也不會有太多深入或底層原理探讨。
本文所引用的資料來源将示于本文尾部。所彙總的内容僅供參考,若有異議望指正。
HBase
HBase 是 Apache Hadoop 中的一個子項目,屬于 bigtable 的開源版本,所實作的語言為Java(故依賴 Java SDK)。HBase 依托于 Hadoop 的 HDFS(分布式檔案系統)作為最基本存儲基礎單元。
HBase在列上實作了 BigTable 論文提到的壓縮算法、記憶體操作和布隆過濾器。HBase的表能夠作為 MapReduce任務的輸入和輸出,可以通過Java API來通路資料,也可以通過REST、Avro或者Thrift的API來通路。
1. 特點
1.1 資料格式
HBash 的資料存儲是基于列(ColumnFamily)的,且非常松散—— 不同于傳統的關系型資料庫(RDBMS),HBase 允許表下某行某列值為空時不做任何存儲(也不占位),減少了空間占用也提高了讀性能。
不過鑒于其它NoSql資料庫也具有同樣靈活的資料存儲結構,該優勢在本次選型中并不出彩。
我們以一個簡單的例子來了解使用 RDBMS 和 HBase 各自的解決方式:
⑴ RDBMS方案:
其中Article表格式:
Author表格式:
⑵ 等價的HBase方案:
對于前端而言,這裡的 Column Keys 和 Column Family 可以看為這樣的關系:
1 2 3 4 5 6 7 8 9 10 11 | columId1 = { //id=1的行 article: { //ColumnFamily-article title: XXX, //ColumnFamily-article下的key之一 content: XXX, tags: XXX }, author: { //ColumnFamily-author name: XXX nickname: XXX } } |
1.2 性能
HStore存儲是HBase存儲的核心,它由兩部分組成,一部分是MemStore,一部分是StoreFiles。
MemStore 是 Sorted Memory Buffer,使用者寫入的資料首先會放入MemStore,當MemStore滿了以後會Flush成一個StoreFile(底層實作是HFile),當StoreFile檔案數量增長到一定門檻值,會觸發Compact合并操作,将多個StoreFiles合并成一個StoreFile,合并過程中會進行版本合并和資料删除,是以可以看出HBase其實隻有增加資料,所有的更新和删除操作都是在後續的compact過程中進行的,這使得使用者的寫操作隻要進入記憶體中就可以立即傳回,保證了HBase I/O的高性能。
1.3 資料版本
Hbase 還能直接檢索到往昔版本的資料,這意味着我們更新資料時,舊資料并沒有即時被清除,而是保留着:
Hbase 中通過 row+columns 所指定的一個存貯單元稱為cell。每個 cell都儲存着同一份資料的多個版本——版本通過時間戳來索引。
時間戳的類型是 64位整型。時間戳可以由Hbase(在資料寫入時自動 )指派,此時時間戳是精确到毫秒的目前系統時間。時間戳也可以由客戶顯式指派。如果應用程式要避免資料版本沖突,就必須自己生成具有唯一性的時間戳。每個 cell中,不同版本的資料按照時間倒序排序,即最新的資料排在最前面。
為了避免資料存在過多版本造成的的管理 (包括存貯和索引)負擔,Hbase提供了兩種資料版本回收方式。一是儲存資料的最後n個版本,二是儲存最近一段時間内的版本(比如最近七天)。使用者可以針對每個列族進行設定。
1.4 CAP類别
屬于CP類型(了解更多)。
2. Node下的使用
HBase的相關操作可參考下表:
在node環境下,可通過 node-hbase 來實作相關通路和操作,注意該工具包依賴于 PHYTHON2.X(3.X不支援)和Coffee。
如果是在 window 系統下還需依賴 .NET framwork2.0,64位系統可能無法直接通過安裝包安裝。
官方示例:
12 13 14 15 16 | var assert = require('assert'); var hbase = require('hbase'); hbase({ host: '127.0.0.1', port: 8080 }) .table('my_table' ) //建立一個Column Family .create('my_column_family', function(err, success){ this.row('my_row') //定位到指定行 .put('my_column_family:my_column', 'my value', function(err, success){ this.get('my_column_family', function(err, cells){ this.exists(function(err, exists){ assert.ok(exists); }); }); }); }); |
資料檢索:
client .table('node_table') .scan({ startRow: 'my_row', //起始行 maxVersions: 1 //版本 }, function(err, rows){ console.log(err, rows); |
另有 hbase-client 也是一個不錯的選擇,具體API參照其文檔。
3. 優缺點
優勢
1. 存儲容量大,一個表可以容納上億行,上百萬列;
2. 可通過版本進行檢索,能搜到所需的曆史版本資料;
3. 負載高時,可通過簡單的添加機器來實作水準切分擴充,跟Hadoop的無縫內建保障了其資料可靠性(HDFS)和海量資料分析的高性能(MapReduce);
4. 在第3點的基礎上可有效避免單點故障的發生。
缺點
1. 基于Java語言實作及Hadoop架構意味着其API更适用于Java項目;
2. node開發環境下所需依賴項較多、配置麻煩(或不知如何配置,如持久化配置),缺乏文檔;
3. 占用記憶體很大,且鑒于建立在為批量分析而優化的HDFS上,導緻讀取性能不高;
4. API相比其它 NoSql 的相對笨拙。
适用場景
1. bigtable類型的資料存儲;
2. 對資料有版本查詢需求;
3. 應對超大資料量要求擴充簡單的需求。
=================================================
Redis
Redis 是一個開源的使用ANSI C語言編寫、支援網絡、可基于記憶體亦可持久化的日志型、Key-Value資料庫,并提供多種語言的API。目前由VMware主持開發工作。
Redis 通常被稱為資料結構伺服器,因為值(value)可以是 字元串(String), 哈希(Hash/Map), 清單(list), 集合(sets) 和 有序集合(sorted sets)五種類型,操作非常友善。比如,如果你在做好友系統,檢視自己的好友關系,如果采用其他的key-value系統,則必須把對應的好友拼接成字元串,然後在提取好友時,再把value進行解析,而redis則相對簡單,直接支援list的存儲(采用雙向連結清單或者壓縮連結清單的存儲方式)。
我們來看下這五種資料類型。
⑴ String
- string 是 Redis 最基本的類型,你可以了解成與 Memcached 一模一樣的類型,一個key對應一個value。
- string 類型是二進制安全的。意思是 Redis 的 string 可以包含任何資料。比如 jpg 圖檔或者序列化的對象 。
- string 類型是 Redis 最基本的資料類型,一個鍵最大能存儲512MB。
執行個體:
redis 127.0.0.1:6379> SET name zfpx OK redis 127.0.0.1:6379> GET name "zfpx" |
在以上執行個體中我們使用了 Redis 的 SET 和 GET 指令。鍵為 name,對應的值為”zfpx”。 注意:一個鍵最大能存儲512MB。
⑵ Hash
- Redis hash 是一個鍵值對集合。
- Redis hash 是一個 string 類型的 field 和 value 的映射表,hash 特别适合用于存儲對象。
redis 127.0.0.1:6379> HMSET user:1 username zfpx password 123 redis 127.0.0.1:6379> HGETALL user:1 1) "username" 2) "zfpx" 3) "password" 4) "123" |
以上執行個體中 hash 資料類型存儲了包含使用者腳本資訊的使用者對象。 執行個體中我們使用了 Redis HMSET, HGETALL 指令,user:1 為鍵值。 每個 hash 可以存儲 232 – 1 鍵值對(40多億)。
⑶ List
Redis 清單是簡單的字元串清單,按照插入順序排序。你可以添加一個元素導清單的頭部(左邊)或者尾部(右邊)。
redis 127.0.0.1:6379> lpush name zfpx1 (integer) 1 redis 127.0.0.1:6379> lpush name zfpx2 (integer) 2 redis 127.0.0.1:6379> lpush name zfpx3 (integer) 3 redis 127.0.0.1:6379> lrange name 0 -1 1) "zfpx3" 2) "zfpx2" 3) "zfpx1" |
清單最多可存儲 232 – 1 元素 (4294967295, 每個清單可存儲40多億)。
⑷ Sets
Redis的Set是string類型的無序集合。 集合是通過哈希表實作的,是以添加,删除,查找的複雜度都是O(1)。
添加一個string元素到 key 對應的 set 集合中,成功傳回1,如果元素已經在集合中傳回0,key對應的set不存在傳回錯誤,指令格式為
sadd key member |
redis 127.0.0.1:6379> sadd school zfpx1 (integer) 0 redis 127.0.0.1:6379> sadd school zfpx2 redis 127.0.0.1:6379> smembers school 1) "zfpx1" |
注意:以上執行個體中 zfpx1 添加了兩次,但根據集合内元素的唯一性,第二次插入的元素将被忽略。 集合中最大的成員數為 232 – 1 (4294967295, 每個集合可存儲40多億個成員)。
⑸ sorted sets/zset
Redis zset 和 set 一樣也是string類型元素的集合,且不允許重複的成員。 不同的是每個元素都會關聯一個double類型的分數。redis正是通過分數來為集合中的成員進行從小到大的排序。
zset的成員是唯一的,但分數(score)卻可以重複。可以通過 zadd 指令(格式如下) 添加元素到集合,若元素在集合中存在則更新對應score
zadd key score member |
redis 127.0.0.1:6379> zadd school 0 zfpx1 redis 127.0.0.1:6379> zadd school 2 zfpx2 redis 127.0.0.1:6379> zadd school 0 zfpx3 redis 127.0.0.1:6379> zadd school 1 zfpx4 redis 127.0.0.1:6379> ZRANGEBYSCORE school 0 100 2) "zfpx3" 3) "zfpx4" 4) "zfpx2" |
Redis資料庫完全在記憶體中,是以處理速度非常快,每秒能執行約11萬集合,每秒約81000+條記錄(測試資料的可參考這篇《Redis千萬級的資料量的性能測試》)。
Redis的資料能確定一緻性——所有Redis操作是原子性(Atomicity,意味着操作的不可再分,要麼執行要麼不執行)的,這保證了如果兩個用戶端同時通路的Redis伺服器将獲得更新後的值。
1.3 持久化
通過定時快照(snapshot)和基于語句的追加(AppendOnlyFile,aof)兩種方式,redis可以支援資料持久化——将記憶體中的資料存儲到磁盤上,友善在當機等突發情況下快速恢複。
1.4 CAP類别
node 下可使用 node_redis 來實作 redis 用戶端操作:
17 18 19 20 | var redis = require("redis"), client = redis.createClient(); // if you'd like to select database 3, instead of 0 (default), call // client.select(3, function() { /* ... */ }); client.on("error", function (err) { console.log("Error " + err); client.set("string key", "string val", redis.print); client.hset("hash key", "hashtest 1", "some value", redis.print); client.hset(["hash key", "hashtest 2", "some other value"], redis.print); client.hkeys("hash key", function (err, replies) { console.log(replies.length + " replies:"); replies.forEach(function (reply, i) { console.log(" " + i + ": " + reply); client.quit(); |
1. 非常豐富的資料結構;
2. Redis提供了事務的功能,可以保證一串 指令的原子性,中間不會被任何操作打斷;
3. 資料存在記憶體中,讀寫非常的高速,可以達到10w/s的頻率。
1. Redis3.0後才出來官方的叢集方案,但仍存在一些架構上的問題(出處);
2. 持久化功能體驗不佳——通過快照方法實作的話,需要每隔一段時間将整個資料庫的資料寫到磁盤上,代價非常高;而aof方法隻追蹤變化的資料,類似于mysql的binlog方法,但追加log可能過大,同時所有操作均要重新執行一遍,恢複速度慢;
3. 由于是記憶體資料庫,是以,單台機器,存儲的資料量,跟機器本身的記憶體大小。雖然redis本身有key過期政策,但是還是需要提前預估和節約記憶體。如果記憶體增長過快,需要定期删除資料。