? ? 從我第一次聽到Nosql這個概念到如今已經走過4個年頭了,但仍然沒有具體的去做過相應的實踐。最近獲得一段學習休息時間,購買了Nosql技術實踐一書,正在慢慢的學習。在主流觀點中,Nosql大體分為4類,鍵值存儲數據庫,列存儲數據庫,文檔型數據庫,圖形數據庫。 今天主要快速的瀏覽了文檔型數據庫中目前市場占有率的最高的MongoDB數據庫。記得初次見到和關注這個數據庫還是我剛來上海的時候,公司將該數據庫作 為新建的項目管理系統的后臺數據庫,當時還是很向往的,只是無緣參與那個項目,也就一直沒有和該數據庫打上交道。接下來簡單的介紹下該數據庫的基本原理和 相關應用,也算是鞏固知識和加強記憶了。大體上快速學習分為兩部分,第一部分為基礎,第二部分為進階。
?
- ?優勢與不足
首 先,MongoDB不需要表結構,它是模式自由的(schema-free),例如{"welcome", "Shanghai"}, {"name", "bibi"}可以放到同一個集合中。那么它是如何在存儲數據的呢?MongoDB在保存數據時會使用Bson的形式,一種json的二進制化形式,并把 它與特定的Key進行關聯。這樣將非常便于程的擴展和維護,在需要增加新字段或者修改字段時只需要修改程序,而不需要修改數據庫的架構,非常的方便。
其 次,MongoDB原生的提供很強的伸縮性,對于web應用,當需要存儲的數據不斷增加時,我們將面對一個很大的問題,如何給數據存儲模塊擴容。在原有的 數據存儲模塊架構中,往往需要通過購買功能更強大的機器,給數據庫服務器升級,但這存在的問題是成本很高,同時升級也受限于當時硬件技術水平的。于此同 時,由于實際web應用中,訪問量并不是相似的,例如在各種活動期間,會出現各種特殊峰值,例如淘寶的11節的第1分鐘的訪問量都已達到千萬級,而在平時 這個值相對小很多。所有這時增加服務器在忙時可能仍然達不到目的,而閑時又會造成大量的浪費,所以伸縮性成為上成為web架構中的最重要的技術指標之一, 這也是當前Nosql技術流行的主要原因。
最后,MongoDB還提供豐富的功能,包括支持輔助索引,支持MapReduce和其他聚合工具,并提供了分布式環境下的高可用,比如自動的在集群中增加和配置節點。
當 然,MongoDB也不是萬能的,實際上也存在一些不足。例如,不支持join查詢和事務處理,數據也不是實時寫入到磁盤的,同時存儲數據時需要預留很大的空間。在實際項目中,需要根據實際的需要進行選擇,當前很多主流網站均使用Sql+NoSql的形式構建數據庫存儲模塊。
- 基本結構
MongoDB中的文檔document相當于Sql數據庫中的一行記錄;多個文檔組成一個集合collection,相當于關系數據庫的表;多個集合組合在一起,就是數據庫database;一個數據庫服務器可以有多個數據庫實例。
- 相關文檔和程序
官方下載地址:https://www.mongodb.org/, 官方目前的版本是3.2,其實2.4以后版本都可以很.NET平臺很好和整合,如果官網下載失敗(常見),就直接網上搜索一個指定版本就好。
官方文檔地址:https://docs.mongodb.org/getting-started/shell/
Mongod:數據庫程序
Mongos:分片控制器
Mongo:Windows下客戶端Client
Mongodump:數據庫的dump工具,支持備份,快照等方式
Mongorestore:從一個dump文件恢復數據庫
Mongoexport:導出單個數據集合到json、CSV等格式
Mongoimport:導出json、CSV等格式數據
Mongofiles:用于到GridFS中,設置和獲取數據文件
Mongostat:顯示性能統計信息
- 安裝步驟(還可以參考博主懶惰的肥兔的博文http://www.cnblogs.com/lzrabbit/p/3682510.html,非常詳細,點個贊)
- 首先在當前目錄中,建立相關目錄:Data保存數據文件,log保存日志信息,etc保存配置文件(mongodb.conf)。
- 在cmd中使用命令,命令如下所示:
|
配置文件內容如下:
dbpath=D:\mongodb\data #數據庫路徑? logpath=D:\mongodb\log\mongodb.log #日志輸出文件路徑? logappend=true #錯誤日志采用追加模式,配置這個選項后mongodb的日志會追加到現有的日志文件,而不是從新創建一個新文件? journal=true #啟用日志文件,默認啟用
port=27017 #端口號 默認為27017 |
?
.NET 下Mongodb的客戶端API可以nuget中很容易的找到,mongoDB .NET 2.0 Driver是使用率最高的,其支持.NET await的異步模型、動態類型dynamic、擴展方法形式的常見Linq查詢(表達式樹)、簡化的日志管理和靜態性能的記錄,使用起來非常的便捷。
在該組件中,client默認就是連接池的方式,所以直接使用單例的client即可,在插入數據時使用BsonDocument,其和json的結構完全一樣,此外在構建Client的連接字符時主要加上mongodb://的協議名就OK。該組件還支持類似automapper之間的功能,將數據庫對象與業務對象的映射,包括自定義屬性映射,緩存元數據等功能具備。
基礎操作文檔地址為:http://mongodb.github.io/mongo-csharp-driver/2.0/getting_started/quick_tour
AutoMap文檔地址:http://mongodb.github.io/mongo-csharp-driver/2.0/reference/bson/mapping/
?
- 性能優化
Mongodb和一般關系型數據庫一樣,也支持查看執行計劃explain,來了解系統實際對索引的使用情況,并根據該情況優化索引,提升查詢性能。在執行計劃結果中,包含如下屬性。
Cursor:返回游標類型(BasicCursor,?BTreeCursor)
Nscanned:被掃描的文件數量
N:返回的文件數量
Millis:耗時(毫秒)
indexBound,表示索引的使用情況,
?
優化器Mongodb database profiler
和 關系型數據庫類似,mongodb也提供慢查詢(就是耗時較長的命令)日志的分析,Mysql有show Query Log與之對應。Profile有3個級別,分別是:0,不開啟;1,記錄慢命令(默認為>100ms);2,記錄所有命令。可以通過以下命令獲取 和設置profile級別和慢命令的執行時間閥值,db.getProfilingLevel(),db.setProfilingLevel(1, 100)。
MongoDb 的profile是記錄在數據庫的系統db中的,位置在system.profile,因此可以通過如下命令獲取所有執行時間大于10ms的 profile記錄,db.system.profile.find({millis:{$gt:5}})。結果字段中,ts表示命令的執行時 間,info為命令詳細信息(類似SQL語句了),reslen表示返回結果集大小,nscanned表示查詢掃描的記錄數,nreturned表示實際 返回的結果集,millis為執行耗時。此外,profile還提供一個show profile命令用于獲取最近5條執行記錄。
當發現掃描的數據集數遠大于返回的記錄集數時,就需要考慮建立索引來加速查詢了,接下來介紹幾條常見的優化策略:
- 在查詢條件和排序字段上建立索引
- 限定返回的結果集skip(),limit(),在這點上mongo真心很贊,因為在互聯網場景下的查詢都是數據庫分頁的
- 只 查詢使用到字段,減少內存消耗,在find()中第一個參數為查詢條件,第二參數為所選字段,與SQL中盡量不要使用select * 類似。例子為db.students.find({}, {name:1}).sort(age: -1).skip(2).limit(3)
- 采 用Capped Collection,類似固定大小的數組,效率高,使用方式為:db.createCollection("mycoll", {capped:true, size:100000})。需要注意的是該集合只支持insert和update操作,不支持一般的delete,只支持類似于SQL中 truncate的drop操作。其數據順序以插入順序為準,如果超過大小,則按照循環數組的形式覆蓋最先的記錄(FIFO)。
- 使用類似存儲過程的Server Side Code Execution來減少網絡傳輸開銷
- 在mongodb query optimizer不能良好工作時(極少),可以通過hint強制索引,在SQLServer, Oracle中也有相似概念,就是不知道有木有包含索引
- 采用profiling
?
- 性能監控
與性能監控相關的常見命令包括:
db.serverStatus(): 查看數據庫實例的運行狀態,信息包括:服務器版本、啟動時間、globalLock中的當前請求(讀/寫)隊列信息、activeClients當前的連 接信息、mem內存占用信息、indexCounters索引被訪問命中的相關信息、服務器的數據量、添刪改查等操作的信息
db.stats(): 查看當前數據庫的狀態,例如當前的test數據庫中集合&對象的數量,數據的可用&當前大小,索引的數量和大小等
Tip:
在windows中有mongostat和mongotop工具用于查看統計信息,在Linux有mongosniff,mongostat等工具,此外還有cacti、Nagios、Zabbix等第三方監控工具。
?
- Replica Sets復制集
MongoDB 支持在多個機器中通過異步復制達到故障轉移和實現冗余,多機器中同一時刻只有一臺用于寫操作,其支持的高可用分為舊的Master-Slave主從復制方 式和Replica Sets復制集方式,推薦使用后者。可以通過rs.status()命令查看復制集狀態,members節點描述復制集相關信息,還可以使用 rs.isMaster()查看相關信息。需要注意的是,在多服務器的集群中,通過一個keyFile來行進識別。
????Replica Sets時通過日志oplog來存儲寫操作的,oplogs.rs是一個固定長度的Capped Collection,存在于local數據庫中。命令db.printReplicationInfo()可以查看oplog的元數據信 息,db.printSlaveReplicationInfo()可以查看slave的同步信息。此外,ReplicaSets的配置信息放在 system.replset中,可以很方便的看到主從的配置信息。
????實 現數據的讀寫分離非常簡單,只需要在從庫中設置db.getMongo().setSlaveOk()即可。ReplicaSets的故障轉移是自動的, 比如我們kill primary的pid, 然后再次查看rs.status()可以看到主服務器的的轉移。在windows中可以使用tasklist查看進程信息,tskill關閉指定pid的 進程,netstat –aon | findstr "27020"可以找到占用指定端口的pid。
????在 提供高可用方案的同時,它也提供負載均衡的解決方案,增減Replica Sets節點非常常見,可以通過rs.add("replset:27023")增加節點,節點增加后自動與主服務器同步數據,可以通過 rs.remove("replset:27024")減去該節點,感覺棒棒噠。
部署Replica Sets
-
在單機多實例的實驗場景下,由于次要的仲裁服務器arbiter不支持使用localhost(會提示重復),因此在C:\Windows\System32\drivers\etc\hosts中添加一行:127.0.0.1 replset
-
添加primary節點和兩個Secondary節點(其中一個為仲裁節點),其實就是把之前的配置復制一遍,在各自的配置文件中加入replSet=rs1,并設置不同的port
-
分別啟動三個節點mongo -f XXX
-
連接primary節點(--port 27020),并通過命令行配置,命令如下所示,當然也可以通過配置文件來設置:
設置配置:config_rs1 = {_id : "rs1",members : [ { _id:0, host:"replset:27020", priority:1 },{ _id:1, host:"replset:27021", priority:1 },{ _id:2, host:"replset:27022", priority:1, "arbiterOnly": true } ]}? 啟用配置:rs.initiate(config_rs1)? 這兒需要注意,這個操作可能需要很長時間,請耐心等待 |
Tip:默認情況primary支持讀寫,而secondary不支持,可以通過rs.slaveOk()命令使得次要節點也能讀寫。
此外,大家也可以查看:http://www.cnblogs.com/jRoger/articles/4708490.html,博主的內容很詳盡。
?
- Sharding分片
這 是一種將海量數據水平擴展的數據庫集群系統,數據分別存儲在Sharding的各個節點上,這就是mongodb源生支持互聯網場景的特征,這部分管理不 再是第三方的一個解決方案而是數據庫自帶的,因而更加便捷高效,這也是我們常說的分庫分表。MongoDb的數據分塊被稱為chunk,每個chunk都 是collection中的一段連續的數據記錄,通常大小為200MB,超出則生成新的數據塊。
構建一個Sharding Cluster需要三種角色:
- Shard Server即存儲實際數據的分片,每一個shard可以是一個mongod實例,也可以是replicaSet,推薦后者
- Config Server,為了將一個特定的Collection存儲在多個Shade中,需要為該Collection指定一個shard key,例如{age:1},shard key決定該條記錄所屬的chunk。Config Servers就是用來存儲所有Shard節點的配置信息、每個chunk的shard key范圍、chunk在各shard的分布情況、該集群中所有DB和Collection的Sharding配置信息。
- Route Process是一個前端路由,客戶端由此接入,然后詢問Config Server需要到哪個Shard上查詢或保存記錄,在連接到相應的Shard進行操作。客戶端只需要將原本發送給mongod的信息發送到 Routing Process,而不用關系操作記錄存儲在哪個Shard。也就是說這個步驟對用戶透明,路由算法由系統提供,比如我們常見的一致性hash算法。
?
搭建步驟:(也可以參照博友蘇若年的博文http://www.cnblogs.com/dennisit/archive/2013/02/18/2916159.html,非常詳細)
- 首 先構建之前之前介紹過的三個角色,route process 1個(port, 27026),config Server 1個(port, 27027),Shard Server 2個(port, 27028, 27029),建立相關目錄和設置相關配置文件。配置文件的差異有:Config配置文件:configsvr=true;Router配置文 件:configdb=localhost:27027(配置服務器地址), chunkSize =100(chunk塊的大小),其他配置基本一致。
- 連 接到Router的admin數據庫, mongo admin --port 27026, 然后運行命令添加兩個shard節 點,db.runCommand({addshard:"localhost:27028"}),db.runCommand({addshard:"localhost:27029"}), 完成Sharding集群的配置。
- 選擇指定數據庫將其狀態設置為可以分片db.runCommand({ enablesharding:"test" })
- 指定分片具體集合,db.runCommand({ shardcollection:"test.users", key:{ _id:1 }}),至此環境搭建完成。
- 可以在該表中插入100000條測試數據,然后通過db.users.stats()查詢該數據集情形,在shards中可以看到具體各個片區的數據量。
?此外,該系統支持添加節點和刪除節點,刪除節點的命令為 db.runCommand({?"removeshard":"localhost:27030"?),printShardingStatus()查 看分片的生效情況,還可以通過db.runCommand({?isdbgrid:1?})命令查看當前實例是否在Sharding環境中。
?
- Replica Sets與Sharding的結合
通 過ReplicaSet和Sharding結合,可以提供可擴展的高可用方案。當業務規模增大時,我們常見的擴展方式有兩種,一種是垂直伸縮,一種是分片 (水平伸縮),前者通過增加服務器的CPU和內存來實現,成本很高,而后者將數據分布到不同的服務器,不同服務器上的數據分塊共同組成一個邏輯數據庫。
圖 2 完整的mongodb高可用可擴展架構
Shards:存儲數據,通過replica sets提供高可用和數據持久性。
Query Routers:當數據庫服務器mongod很多時,推薦增加Router來分發大量的客戶請求。Mongos是一個輕量級的進程不需要數據目錄,
Config servers:存儲集群元數據,包含集群數據集與各個片區的映射,在3.2版后支持將config-servers部署為replica set,避免單點故障,不再推薦原有的三鏡像形式的配置服務器實例。
MongoDb 通過shard key對數據進行分區,系統默認使用range based partition或hash based partition。前者通過區間分布,因而相近數據分布較近,范圍查詢的效率更高,于此同時由于分布不均勻,當請求集中在其中一臺服務器時,將出現過量 負載;后者通過hash函數分布,分布比較分散,負載均勻,但對于范圍查找相對較慢。
系 統提供后臺運行的splitting功能,當Shard不斷增大超過閥值,系統將會把它分成等量的兩部分。后臺balancing進程管理chunk的遷 移,當負載均衡器發現某個shard中chunk過多時,會將部分chunk轉移到chunk數最少的服務器,值得一提的是,只有在源shard的 chunk遷移到目的shard后,才會刪除源上的chunk,因此在遷移過程中出現問題并不會導致數據丟失。
?
在Windows上詳細構建步驟可參照博友左鹽的博文http://www.cnblogs.com/spnt/archive/2012/07/26/2610070.html,以及博友Geek_Ma的博文http://www.cnblogs.com/geekma/archive/2013/05/16/3081532.html。
?
- 基礎查詢
有 幾點需要注意:不需要預先創建集合,在第一次插入數據時會自動創建;文檔中可以存儲任意類型數據,不需要類似alter table的語句來改變結構;每次插入時都有一個_id,類型為OBjectId,其實就是GUID了,便于分布式環境下的唯一標示,當然它也可以是 int或long等類型。
操作類別 | 實例 | 備注 |
插入 | j={name, "bibi"};t={x : 3};db.things.save(j); db.things.save(t);db.things.find(); |
|
選擇數據庫 | Use test |
|
修改 | Db.things.update({name,"mongo"}, {$set:{name:"mongo_new"}}); | ?? |
刪除 | Db.things.remove({name:"mongo_new"}); | ?? |
普通查詢 | var cursor = db.things.find();while(cursor.hasNext()) printjson(cursor.next()); | 獲得游標,遍歷游標。注意在數據集合很大時可能會引起內存溢出 |
?? | Db.things.find().forEach(printjson) | ?? |
?? | Var arr = db.things.find().toArray();Arr[5]; | ?? |
條件查詢 | Db.things.find({x:4}, {j:true}).forEach(printjson); | ?? |
FindOne | Printjson(db.things.findOne({name:"mongo"})); | ?? |
limit | Db.things.find().limit(3); | ?? |
? ?
- 高級查詢
操作符 | 實例 | 備注 |
條件操作符 | Db.collection.find({"field":{$gt:value}});Db.collection.find({"field":{$lt:value}});Db collection.find({"field":{$gte:value}});Db.collection.find({"field":{$lte:value}}); | Field>valueField<valueField>=valueField<=value |
$all | Db.users.find({age:{$all:[6, 8]}}); | 必須滿足[]內所有值 |
$exists | Db.things.find({age:{$exists:true}});Db.things.find({age:{$exists:false}}); | 查詢存在age字段的記錄查詢不存在age字段的記錄 |
Null值的處理 | Db.collection.find(age:null)}Db.collection.find(age:{$in:[null], $exists:true})} | 這兒要注意,在只用null作為判斷條件是,還會把不存在age字段的記錄找出來 |
$mod | Db.collection.find({age:{$mod:[10, 1]}}) | 取模運算 |
$ne | Db.things.find({x:{$ne:3}}); | 不等于 |
$in | Db.users.find({age:{$in:[2,4,6]}}); | 包含 |
$nin | Db.users.find({age:{$nin:[1,3]}}) | 不包含 |
$size | {name:'bibi', age:26, luck_number:[3,7,9]},db.users.find({luck_number:{$size: 3}}) | 數組元素個數 |
正則表達式匹配 | Db.users.find({name:{$not:/^B.*/}}); | 查詢不匹配name=B*帶頭的記錄 |
Javascript查詢和$where查詢 | Db.collection.find({a:{$gt:3}});Db.collection.find($where:"this.a>3");Db.collection.find(this.a>3");f=function(){return this.a>3}db.collection.find(f); | 查詢a大于3的數據 |
count | Db.users.find().count();Db.user.find().skip(10).limit(5).count();Db.user.find().skip(10).limit(5).count(true); | 查詢記錄條數還是返回的所有記錄數加true也能限制數量 |
Skip | Db.users.find().skip(3).limit(5) | 相當于limit(3, 5) |
sort | Db.colletion.find().sort({age:1});Db.colletion.find().sort({age:-1}); | 按升序進行排序按降序進行排序 |
游標 | For(var c = db.t3.find();c.hasNext();){Printjson(c.next());}Db.t3.find().forEach(function(u){printjson(u);}); | ?? |
? ?
- 統計Map/Reduce
Map/Reduce 這個概念已經存在了很多年,記得有個印度工程時通過做不同口味的番茄醬的理解風趣幽默的為妻子解釋了這個概念,主體的意思就是分工然后匯總。在這里 Map/Reduce相當于MySQL中的"group by",使用過程需要實現Map函數和Reduce函數。
函數名 | 實例 | 備注 |
前提條件 | Db.students.insert({classid:1, age:14, name:'Tom'})Db.students.insert({classid:2, age:27, name:'Bibi'}) | 插入班級1,2共8條記錄. |
Map | m=function(){emit(this.classid, 1)} | Map函數必須調用emit(key, value)返回鍵值對,使用this訪問當前待處理的Document.相當于SQL的分組操作,其中的this.classid分組屬性,1是用于聚合的屬性或值 |
Reduce | r=function(key, value){var x =0;values.forEach(function(v){x+=v});return x;} | Reduce函數接受的參數類似Group效果,將Map返回的鍵值序列組合成{key, [value1, value2, value3..]}傳遞給reduce.?相當于SQL的聚合操作,這兒的x+=v實際就是SQL中的count(*) |
Result | Res=db.runcommand({mapreduce:"students",map:m,reduce:r,out:"student_res"}); | 相當于分組聚合操作的執行,并將結果集輸出到指定的Collection獲得結果:{"_id": 1, "value":3} |
finalize | F=function(key, value){return {classid:key, count:value};} Res=db.runcommand({…(同上)finalize:f}); | 利用finalize()我們可以對reduce()的結果做進一步的處理。結果變為如下形式:{"classid": 1, "count":3}。類似于SQL中的取別名格式化輸出等操作。 |
options | Res=db.runcommand({…(同上)Query:{age:{$lt:10}}}); | 可選項,例如過濾操作,只取age<10的數據。相當于where操作,注意不是having。 |
? ?
- 索引
MongoDB提供了多樣性的索引支持,索引信息被保存在system.indexes中,且默認總是為_id創建的索引。
操作符 | 實例 | 備注 |
基礎索引 | Db.t3.ensureIndex({age:1});Db.t3.getIndexes(); | 按升序排序的索引。注意,1表示升序,-1表示降序查看有哪些索引,默認情況下,_id為創建表時自動創建的索引 |
?? | Db.t3.ensureIndex({age:1}, {background:true}); | 當系統已有大量數據時,創建索引非常耗時,我們可以在后臺執行 |
文檔索引 | Db.factories.insert({name:"SORY", addr:{city:"Shanghai", state:"China"}});Db.factories.ensureIndex({addr:1}); | 索引可以是任何類型的字段,甚至文檔。注意索引建立的順序,這點和關系型數據庫一樣,錯誤的select順序可能造成不觸發索引 |
組合索引 | Db.factories.ensureIndex({"addr.city":1, "addr.state":1}); | ?? |
唯一索引 | Db.users.ensureIndex({firstname:1, lastname:1}, {unique:true}); | 注意,如果建立索引所選字段的既有值有重復的,是無法建立唯一索引的。 |
強制使用索引 | Db.t5.find({age:{<: 30}}).hint(name:1, age:1).explain(); | ?通過執行計劃查看,SQL Server中也有相似的概念,強制走索引 |
刪除索引 | Db.t1.dropIndexesDb.t1.dropIndex({firstname:1}) | 刪除t3表的所有索引刪除指定索引 |
?
Tip:
博文主要供個人基礎學習使用,若有疏漏,忘見諒。文中部分圖片來之于mongodb官網https://docs.mongodb.org/manual/。
參考資料:
- 皮雄軍. NoSQL數據庫技術實戰[M].?北京:清華大學出版社, 2015.