| 1NoSQL簡述 CAP(Consistency,Availabiity,Partitiontolerance)理論告訴我們,一個分布式系統不可能滿足一致性,可用性和分區容錯性這三個需求,最多只能同時滿足兩個。關系型數據庫通過把更新操作寫到事務型日志里實現了部分耐用性,但帶來的是寫性能的下降。MongoDB等NoSQL數據庫背后蘊涵的哲學是不同的平臺應該使用不同類型的數據庫,MongoDB通過降低一些特性來達到性能的提高,這在很多大型站點中是可行的。因為MongoDB是非原子性的,所以如果如果應用需要事務,還是需要選擇MySQL等關系數據庫。 NoSQL數據庫,顧名思義就是打破了傳統關系型數據庫的范式約束。很多NoSQL數據庫從數據存儲的角度看也不是關系型數據庫,而是key-value數據格式的hash數據庫。由于放棄了關系數據庫強大的SQL查詢語言和事務一致性以及范式約束,NoSQL數據庫在很大程度上解決了傳統關系型數據庫面臨的諸多挑戰。 在社區中,NoSQL是指“notonly sql”,其特點是非關系型,分布式,開源,可水平擴展,模式自由,支持replication,簡單的API,最終一致性(相對于即時一致性,最終一致性允許有一個“不一致性窗口”,但能保證最終的客戶都能看到最新的值)。 2MongoDB簡介 mongo取自“humongous”(海量的),是開源的文檔數據庫──nosql數據庫的一種。 MongoDB是一種面向集合(collection)的,模式自由的文檔(document)數據庫。 面向集合是說數據被分成集合的形式,每個集合在數據庫中有惟一的名稱,集合可以包含不限數目的文檔。除了模式不是預先定義好的,集合與RDBMS中的表概念類似,雖然二者并不是完全對等。數據庫和集合的創建是“lazy”的,即只有在第一個document被插入時集合和數據庫才真正創建——這時在磁盤的文件系統里才能看見。 模式自由是說數據庫不需要知道存放在集合中的文檔的結構,完全可以在同一個集合中存放不同結構的文檔,支持嵌入子文檔。 文檔類似于RDBMS中的記錄,以BSON的格式保存。BSON是BinaryJSON的簡稱,是對JSON-like文檔的二進制編碼序列化。像JSON(JavaScriptObject Notation)一樣,BSON支持在對象和數組內嵌入其它的對象和數組。有些數據類型在JSON里不能表示,但可以在BSON里表示,如Date類型和BinData(二進制數據),Python原生的類型都可以表示。與ProtocalBuffers(Google開發的用以處理對索引服務器請求/應答的協議)相比,BSON模式更自由,所以更靈活,但這樣也使得每個文檔都要保存字段名,所以空間壓縮上不如ProtocolBuffers。 BSON第一眼看上去像BLOB,但MongoDB理解BSON的內部機制,所以MongoDB可以深入BSON對象的內部,即使是嵌套的對象,這樣MongoDB就可以在頂層和嵌套的BSON對象上建立索引來應對各種查詢了。 MongoDB可運行在Linux、Windows和OSX平臺,支持32位和64位應用,默認端口為27017。推薦運行在64位平臺,因為MongoDB為了提高性能使用了內存映射文件進行數據管理,而在32位模式運行時支持的最大文件為2GB。 MongoDB查詢速度比MySQL要快,因為它cache了盡可能多的數據到RAM中,即使是non-cached數據也非常快。當前MongoDB官方支持的客戶端API語言就多達8種(C|C++|Java|Javascript|Perl|PHP|Python|Ruby),社區開發的客戶端API還有Erlang、Go、Haskell...... 3術語介紹 數據庫、集合、文檔 每個MongoDB服務器可以有多個數據庫,每個數據庫都有可選的安全認證。數據庫包括一個或多個集合,集合以命名空間的形式組織在一起,用“.”隔開(類似于JAVA/Python里面的包),比如集合blog.posts和blog.authors都處于"blog"下,不會與bbs.authors有名稱上的沖突。集合里的數據由多個BSON格式的文檔對象組成,document的命名有一些限定,如字段名不能以"$"開頭,不能有".",名稱"_id"被保留為主鍵。 如果插入的文檔沒有提供“_id”字段,數據庫會為文檔自動生成一個ObjectId對象作為“_id”的值插入到集合中。字段“_id”的值可以是任意類型,只要能夠保證惟一性。BSONObjectID是一個12字節的值,包括4字節的時間戳,3字節的機器號,2字節的進程id以及3字節的自增計數。建議用戶還是使用有意義的“_id”值。 MongoDb相比于傳統的SQL關系型數據庫,最大的不同在于它們的模式設計(SchemaDesign)上的差別,正是由于這一層次的差別衍生出其它各方面的不同。 如果將關系數據庫簡單理解為由數據庫、表(table)、記錄(record)三個層次概念組成,而在構建一個關系型數據庫的時候,工作重點和難點都在數據庫表的劃分與組織上。一般而言,為了平衡提高存取效率與減少數據冗余之間的矛盾,設計的數據庫表都會盡量滿足所謂的第三范式。相應的,可以認為MongoDb由數據庫、集合(collection)、文檔對象(Document-oriented、BSON)三個層次組成。MongoDb里的collection可以理解為關系型數據庫里的表,雖然二者并不完全對等。當然,不要期望collection會滿足所謂的第三范式,因為它們根本就不在同一個概念討論范圍之內。類似于表由多條記錄組成,集合也包含多個文檔對象,雖然說一般情況下,同一個集合內的文檔對象具有相同的格式定義,但這并不是必須的,即MongoDb的數據模式是自由的(schema-free、模式自由、無模式),collection中可以包含具有不同schema的文檔記錄,支持嵌入子文檔。 4MongoDB資源消耗 考慮到性能的原因,mongo做了很多預分配,包括提前在文件系統中為每個數據庫分配逐漸增長大小的文件集。這樣可以有效地避免潛在的文件系統碎片,使數據庫操作更高效。 一個數據庫的文件集從序號0開始分配,0,1...,大小依次是64M,128M,256M,512M,1G,2G,然后就是一直2G的創建下去(32位系統最大到512M)。所以如果上一個文件是1G,而數據量剛好超過1G,則下一個文件(大小為2G)則可能有超過90%都是空的。 如果想使磁盤利用更有效率,下面是一些解決方法: 1. 只建立一個數據庫,這樣最多只會浪費2G。 2. 每個文檔使用自建的“_id”值而不要使用默認的ObjectId對象。 3. 由于每個document的每個字段名都會存放,所以如果字段名越長,document的數據占用就會越大,因此把字段名縮短會大大降低數據的占用量。如把“timeAdded”改為“tA”。 Mongo使用內存映射文件來訪問數據,在執行插入等操作時,觀察mongod進程的內存占用時會發現量很大,當使用內存映射文件時是正常的。并且映射數據的大小只出現在虛擬內存那一列,常駐內存量才反應出有多少數據cached在內存中。 【按照mongodb官方的說法,mongodb完全由系統內核進行內存管理,會盡可能的占用系統空閑內存,用free可以看到,大部分內存都是作為iocache被占用的,而這部分內存是可以釋放出來給應用使用的。】 5交互式shell mongo類似于MySQL中的mysql進程,但功能遠比mysql強大,它可以使用JavaScript語法的命令從交互式shell中直接操作數據庫。如查看數據庫中的內容,使用游標循環查看查詢結果,創建索引,更改及刪除數據等數據庫管理功能。下面是一個在mongo中使用游標的例子: | > for(var cur= db.posts.find(); cur.hasNext();) {
...print(tojson(cur.next()));
... } | 輸出: | {"_id" :ObjectId("4bb311164a4a1b0d84000000"),"date" : "Wed Mar 3117:05:23 2010","content" :"blablablabla","author" :"navygong","title" : "the firstblog"} | ...其它的documents。 6一般功能 6.1插入 客戶端把數據序列化為BSON格式傳給DB后被存儲在磁盤上,在讀取時數據庫幾乎不做什么改動直接把對象返回給客戶端,由client完成unserialized。如: > doc ={'author': 'joe', 'created': new Date('2010, 6, 21'), 'title':'Yet another blogpost', 'text': 'Here is the text...', 'tags': ['example', 'joe'], 'comments':[{'author': 'jim', 'comment': 'I disgree'}, {'author': 'navy', 'comment': 'Goodpost'}], '_id': 'test_id'} >db.posts.insert(doc) 6.2查詢 基本上你能想到的查詢種類MongoDB都支持,如等值匹配,<,<=,>,>=,$ne,$in,$mod,$all,$size[1],$exists,$type[2],正則表達式匹配,全文搜索,......。還有distinct(),sort(),count(),skip()[3],group()[4],......。這里列表的查詢中很多用法都和一般的RDBMS不同。 [1]匹配一個有size個元素的數組。如db.things.find({a:{$size: 1}})能夠匹配文檔{a:["foo"]}。 [2] 根據類型匹配。db.things.find({a: {$type : 16}})能夠匹配所有a為int類型的文檔。BSON協議中規定了各種類型對應的枚舉值。 [3]指定跳過多少個文檔后開始返回結果,可以用在分頁中。如:db.students.find().skip((pageNumber-1)*nPerPage).limit(nPerPage).forEach(function(student) { print(student.name + "<p>"); } )。 [4] 在shardedMongoDB配置環境中應該應該使用map/reduce來代替group()。 6.3刪除 可以像查詢一樣指定條件來刪除特定的文檔。 6.4索引 可以像在一般的RDBMS中一樣使用索引。提供了建立(一般、惟一、組合)索引、刪除索引、重建索引等各種方法。索引信息保存在集合“system.indexes”中。 6.5map/reduce MongoDB提供了map/reduce方法來進行數據的批處理及聚集操作。和Hadoop的使用類似,從集合中接收輸入,結果輸出到另一個集合。如果你需要使用group,map/reduce會是個不錯的選擇。但MongoDB中的索引和標準查詢不是使用map/reduce,而是與MySQL相似。 7模式設計 Mongo式的模式設計 使用Mongo有很多種方式,你本能上可能會像使用關系型數據庫一樣去使用。當然這樣也可以工作得很好,但卻沒能發揮出Mongo的真正威力。Monog是專門設計為富對象模型(richobject model)使用的。 例如:如果你建立了一個簡單的在線商店并且把產品信息存儲在關系型數據庫中,那你可能會有兩個像這樣的表: item item_features | sku | feature_name | feature_value | 你進行了范式處理因為不同的物品有不同的特征,這樣你不用建立一個包含所有特征的表了。在Mongo中你也可以像上面那樣建立兩個集合,但像下面這樣存儲每種物品會更有效。 | item : {"title" : <title> ,"price" : <price> ,"sku" : <sku> ,"features" : {"optical zoom" : <value> ,...}
} | 因為只要查詢一個集合就能取得一件物品的所有信息,而這些信息都保存在磁盤上同一個地方,因此大大提高了查詢的速度。如果你想插入或更新一種特征,如db.items.update({ sku : 123 } , { "$set" : { "features.zoom" :"5" } } ),也不必在磁盤上移動整個對象,因為Mongo為每個對象在磁盤上預留了空間來適應對象的增長。 8嵌入與引用 以一實例來說,假設需要設計一個小型數據庫來存儲“學生、地址、科目、成績”這些信息,那么關系型數據庫的設計如圖1所示,而key-value型數據庫的設計則可能如圖2所示。 圖1關系型的數據庫設計 圖2key-value型的數據庫設計 對比圖1和圖2,在關系型的數據庫設計里劃分出了4個表,而在key-value型的數據庫設計里卻只有兩個集合。如果說集合與表一一對應的話,那么圖2中應該也有4個集合才對,把本應該是集合的address和scores直接合入了集合students中,原因在于在key-value型的數據庫里,數據模式是自由的。 以scores來說,在關系型的數據庫設計中將其單獨成一個表是因為student與score是一對多的關系,如果將score合入student表,那么就必須預留最多可能的字段,這會存在浪費,并且當以后新增一門課程時擴展困難,因此一般都會將score表單獨出來。而對于key-value型的數據庫就不同了,其scores字段就是一個BSON,該BSON可以只有一個for_course,也可以有任意多個for_course,其固有的模式自由特性使得它可以將score包含在內而無需另建一個score集合。 對于與student為一對一關系的address表也可以直接合入student,無需擔心address的擴展性,當以后需要給address新增一個province字段,直接在數據插入時加上這個值即可。 當然,對于與student成多對多關系course表,為了減少數據冗余,可以將course建立為一個集合,同關系型的數據庫設計中類似。 students文檔中嵌入了address文檔和scores文檔,scores文檔的“for_course”字段的值是指向courses集合的文檔的引用。如果是關系型數據庫,需要把“scores”作為一個單獨的表,然后在students表中建立一個指向“scores”的外鍵。所以Mongo模式設計中的一個關鍵問題就是“是值得為這個對象新建一個集合呢,還是把這個對象嵌入到其它的集合中”。在關系型數據庫中為了范式的要求,每個子項都要建一個單獨的表,但在Mongo中使用嵌入式對象更有效,所以你應該給出不使用嵌入式對象而單獨建一個集合的理由。 為什么說引用要慢些呢,以上面的students集合為例,比如執行: print(student.scores[0].for_course.name ); 如果這是第一次訪問scores[0],那些客戶端必須執行: student.scores[0].for_course= db.courses.findOne({_id:_course_id_to_find_}); //偽代碼 所以每一次遍歷引用都要對數據庫進行一次這樣的查詢,即使所有的數據都在內存中。再考慮到從客戶端到服務器端的種種延遲,這個時間也不會低。 有一些規則可以決定該用嵌入還是引用: 1. 第一個類對象,也就是處于頂層的,往往應該有自己的集合。 2. 排列項詳情對象應該用嵌入。 3. 處于被包含關系的應該用嵌入。 4. 多對多的關系通常應該用引用。 5. 數據量小的集合可以放心地做成一個單獨的集合,因為整個集合可以很快地cached。 6. 要想獲得嵌入式對象的系統級視圖會更困難一些。如上面的“Scores”如果不做成嵌入式對象可以更容易地查詢出分數排名前100的學生。 7. 如果嵌入的是大對象,需要留意到BSON對象的4M大小限定(后面會講到)。 8. 如果性能是關鍵就用嵌入。 下面是一些示例: 1.Customer/Order/Order Line-Item cutomers和orders應該做成一個集合,line-items應該以數組的形式嵌入在order中。 2. 博客系統 posts應該是一個集合;author可以是一個單獨的集合,如果只需記錄作者的email地址也可以以字段的方式存在于posts中;comments應該做成嵌入的對象。 9GridFS GridFS是MongoDB中用來存儲大文件而定義的一種文件系統。MongoDB默認是用BSON格式來對數據進行存儲和網絡傳輸。但由于BSON文檔對象在MongoDB中最大為4MB,無法存儲大的對象。即使沒有大小限制,BSON也無法滿足對大數據集的快速范圍查詢,所以MongoDB引進了GridFS。 9.1GridFS表示的對象信息 1. 文件對象(類GridFSFile 的對象)的元數據信息。結構如下 | {"_id" : <unspecified>, // unique ID for this file"filename" : data_string, // human name for the file"contentType" : data_string, // valid mime type for the object"length" : data_number, // size of the file in bytes"chunkSize" : data_number, // size of each of the chunks. Default is 256k"uploadDate" : data_date, // date when object first stored"aliases" : data_array ofdata_string, // optional array ofalias strings"metadata" : data_object, // anything the user wants tostore"md5" : data_string //result of running "filemd5"command on the file's chunks
} | 如下是put進去的一個文件例子: | {
_id:ObjId(4bbdf6200459d967be9d8e98),
filename:"/home/hjgong/source_file/wnwb.svg",
length: 7429,
chunkSize:262144,
uploadDate: newDate(1270740513127),
md5:"ccd93f05e5b9912c26e68e9955bbf8b9"
} | 2. 數據的二進制塊以及一些統計信息。結構如下: | {"_id": <unspecified>, // object id of the chunk in the _chunkscollection"files_id":<unspecified>, // _id value ofthe owning {{files}} collection entry"n": data_number, // "chunk number" -starting with 0"data": data_binary (type 0x02), // binary data for chunk
} | 因此使用GridFS可以儲存富媒體文件,同時存入任意的附加信息,因為這些信息實際上也是一個普通的collection。以前,如果要存儲一個附件,通常的做法是,在主數據庫中存放文件的屬性同時記錄文件的path,當查詢某個文件時,需要首先查詢數據庫,獲得該文件的path,然后從存儲系統中獲得相應的文件。在使用GridFS時則非常簡單,可以直接將這些信息直接存儲到文件中。比如下面的Java代碼,將文件file(file可以是圖片、音頻、視頻等文件)儲存到db中: 其中該方法的第一個參數的類型還可以是InputStream,byte[],從而實現多個重載的方法 9.2GridFS管理 MongoDB提供的工具mongofiles可以從命令行操作GridFS。如: ./mongofiles -host localhost:1727 -unavygong -p 111 put ~/source_file/wnwb.svg 每種語言提供的MongoDB客戶端API都提供了一套方法,可以像操作普通文件一樣對GridFS文件進行操作,包括read(),write(),tell(),seek()等。 10Replication(復制) Mongo提供了兩種方式的復制:簡單的master-slave配置及replicapair的概念。 如果安全認證被enable,不管哪種replicate方式,都要在master/slave中創建一個能為各個database識別的用戶名/密碼。認證步驟如下: slave先在local.system.users里查找一個名為"repl"的用戶,找到后用它去認證master。如果"repl"用戶沒有找到,則使用local.system.users中的第一個用戶去認證。local數據庫和admin數據庫一樣,local中的用戶可以訪問整個dbserver。 10.1master-slave模式 一個server可以同時為master和slave。一個slave可以有多個master,這種方式并不推薦,因為可能會產生不可預期的結果。 在該模式中,一般是在兩個不同的機器上各部署一個MongDB實例,一個為master,另一作為slave。將MongoDB作為master啟動,只需要在命令行輸入: ./mongod --master 然后主服務進程將會在數據庫中創建一個集合local.oplog.$main,該collection主要記錄了事務日志,即需要在slave執行的操作。 而將MongoDB作為slave啟動,只需要在命令行輸入: ./mongod --slave --source <masterhostname>[:<port>] port不指定時即使用默認端口,masterhostname是master的IP或master機器的FQDN。 其他配置選項: --autoresync:自動sync,但在10分鐘內最多只會進行一次。 --oplogSize:指定master上用于存放更改的數據量,如果不指定,在32位機上最少為50M,在64位機上最少為1G,最大為磁盤空間的5%。 10.2replicapairs模式 以這種方式啟動后,數據庫會自動協商誰是master誰是slave。一旦一個數據庫服務器斷電,另一個會自動接管,并從那一刻起起為master。萬一另一個將來也出錯了,那么master狀態將會轉回給第一個服務器。以這種復制方式啟動本地MongoDB的命令如下: ./mongod --pairwith <remoteserver> --arbiter <arbiterserver> 其中remoteserver是pair里的另一個server,arbiterserver是一個起仲裁作用的Mongo數據庫服務器,用來協商pair中哪一個是master。arbiter運行在第三個機器上,利用“平分決勝制”決定在pair中的兩臺機器不能聯系上對方時讓哪一個做master,一般是能同arbiter通話的那臺機器做master。如果不加--arbiter選項,出現網絡問題時兩臺機器都作為master。命令db.$cmd.findOne({ismaster:1})可以檢查當前哪一個database是master。 pair中的兩臺機器只能滿足最終一致性。當replicapair中的一臺機器完全掛掉時,需要用一臺新的來代替。如(n1,n2)中的n2掛掉,這時用n3來代替n2。步驟如下: 1. 告訴n1用n3來代替n2:db.$cmd.findOne({replacepeer:1}); 2. 重啟n1讓它同n3對話:./mongod--pairwith n3 --arbiter <arbiterserver> 3. 啟動n3:./mongod--pairwith n1 --arbiter <arbiterserver>。 在n3的數據沒有同步到n1前n3還不能做master,這個過程長短由數據量的多少決定。 10.3受限的master-master復制 Mongo不支持完全的master-master復制,通常情況下不推薦使用master-master模式,但在一些特定的情況下master-master也可用。master-master也只支持最終一致性。配置master-master只需運行mongod時同時加上--master選項和--slave選項。如下: $ nohup mongod --dbpath /data1/db --port 27017 --master --slave --source localhost:27018 > /tmp/dblog1 & $ nohup mongod --dbpath /data2/db --port 27018 --master --slave --source localhost:27017 > /tmp/dblog2 & 這種模式對插入、查詢及根據_id進行的刪除操作都是安全的。但對同一對象的并發更新無法進行。 11Sharding(分片) 11.1sharding介紹 MongoDB包括一個自動分片的的模塊(“mongos”),從而可以構建一個大的水平可擴展的數據庫集群,可以動態地添加和移走機器。如下是一個數據庫集群的示意圖: mongod:數據庫服務器進程,類似于mysqld。 shards:每個shard有一個或多個mongod,通常是一個master,多個slave組成replication。數據由集合按一個預定的順序劃分,某一個范圍的數據被放到一個特定的shard中,這樣可以通過shard的key進行有效的范圍查詢。 shard keys:用于劃分集合,格式類似于索引的定義,也是把一個或多個字段作為key,以key來分布數據。如:{name : 1 (1代表升序,-1代表降序)}、{_id : 1 }、{ lastname : 1,firstname : 1 }、{ tag : 1, timestamp :-1 }。如果有100萬人同名,可能還需要劃分,因為放到一個塊里太大了,這時定義的sharkey不能只有一個name字段了。劃分能夠保證相鄰的數據存儲在一個server(當然也在相同的塊上)。 chunks:是一個集合里某一范圍的數據,(collection,minkey, maxkey)描述了一個chunk。塊的大小有限定,當塊里的數據超過最大值,塊會一分為二。如果一個shard里的數據過多(添加shard時,可以指定這個shard上可以存放的最大數據量maxSize),就會有塊遷移到其它的shard。同樣,當添加新的server時,為了平衡各個server的負載,也會遷移chunk過去。 config server(配置服務器):存儲了集群的元信息,包括每一個shard、一個shard里的server、以及每一個chunk的基本信息。其中主要是chunk的信息,每個configserver中都有一份所有chunk信息的完全拷貝。使用兩階段提交協議來保證配置信息在configserver間的一致。mongos:可以認為是一個“數據庫路由器”,用以協調集群的各個部分,使它們看起來像一個系統。mongos沒有固定的狀態,可以在server需要的時候運行。mongos啟動后會從configserver里取出元信息,然后接收客戶請求,把請求路由到合適的server,得到結果后送回客戶。一個系統可以有多個mongos例程,每個例程都需要內存來存儲元信息。例程間不需協同工作,每個mongos只需要協同shardservers和config servers工作即可。當然shardservers間也會彼此對話,也會同config servers對話。 11.2sharding的配置和管理 mongod的啟動選項中也包含了與sharding相關的參數,如--shardsvr(聲明這是一個sharddb),--configsvr(聲明這是一個configdb)。mongos的啟動選項--configdb指定configserver的位置。下面的鏈接地址是一個簡單的sharding配置例子:http://www.mongodb.org/display/DOCS/A+Sample+Configuration+Session。 像安全和認證一樣,如果要sharding,先要允許一個數據庫sharding,然后要指定數據庫里集合的分片方式,這些都有相應的命令可以完成。 12JavaAPI簡介 要使用Java操作MongoDB,在官網上下載jar包,目前最新的版本是:mongo-2.0.jar。首先介紹一下比較常用的幾個類: Mongo:連接服務器,執行一些數據庫操作的選項,如新建立一個數據庫等; DB:對應一個數據庫,可以用來建立集合等操作; DBCollection:對應一個集合(類似表),可能是我們用得最多的,可以添加刪除記錄等; DBObject接口和BasicDBObject對象:表示一個具體的記錄,BasicDBObject實現了DBObject,因為是key-value的數據結構,所以用起來其實和HashMap是基本一致的; DBCursor:用來遍歷取得的數據,實現了Iterable和Iterator。 下面以一段簡單的例子說明: 13MongoDB實例分析 下面通過一個實例說明如何用MongoDB作為數據庫。該實例中有一個user實體,包含一個name屬性,每個user對應一到多個圖片image。按照關系型數據庫設計,可以設計一個user表和一個image表,其中image表中有一個關聯到user表的外鍵。如果將這兩個表對應為兩個collection,即image對應的collection中的每一個document都有一個key,其value是該image關聯的user。但為了體現MongoDB的效率,即MongoDB是schema-free的,而且支持嵌入子文檔,因此在實現時,將一個user發布的image作為該user的子文檔嵌入其中,這樣只需要定義一個collection,即userCollection。如下圖所示: 對于圖片等文件,可以存儲在文件系統中,也可以存儲在數據庫中。因此下面分兩種情況實現。 13.1圖片保存在文件系統中 這種情況下,圖片實體中需要記錄圖片的路徑uri,因此Image類的定義如下: 因為在MongoDB中,當保存的對象沒有設置ID時,mongoDB會默認給該條記錄設置一個ID("_id"),因此在類中沒有定義id屬性(下同)。 因為一個user對應多個image,所以在user實體中需要記錄對應的image。如下: 在main函數中實現如下功能:首先定義一個user(假設id為1),其對應3張圖片,然后將該user插入userCollection中。然后,通過查詢查找到該user(根據id),再發布第4張圖片,更新該user,然后打印出其信息。部分代碼如下: 程序運行后,在控制臺打印出的信息如下: 從該結果容易看出,用戶user有兩個屬性“_id”和“Name”,而且ImageList作為其子文檔(數組)嵌入其中,該數組中是3個圖片,每個圖片仍然是bson格式。 13.2圖片保存在數據庫中 這種情況下,圖片實體只需要存儲文件名即可,因此Image2類的定義如下: User2類和上面類似,如下所示: 實現了類MongoTest2,其功能仍然是一個user對應3個圖片,存入數據庫中后,通過查詢得到該user后,再插入第4幅圖片,然后打印出信息。同時為了演示文件的查詢,對存入MongoDB中的圖片進行了查詢并打印出其部分元數據信息。部分代碼如下所示: 運行程序,控制臺打印出的結果如下: 14MongoDB常用API總結 類轉換 當把一個類對象存到mongoDB后,從mongoDB取出來時使用setObjectClass()將其轉換回原來的類。 | public class Tweet implements DBObject { /* ... */
}
Tweet myTweet = new Tweet();
myTweet.put("user", "bruce");
myTweet.put("message", "fun");
myTweet.put("date", new Date());
collection.insert(myTweet);
//轉換
collection.setObjectClass(Tweet.class);
Tweet myTweet = (Tweet)collection.findOne(); | 默認ID 當保存的對象沒有設置ID時,mongoDB會默認給該條記錄設置一個ID("_id")。 當然你也可以設置自己指定的ID,如:(在mongoDB中執行用db.users.save({_id:1,name:'bruce'});) BasicDBObject bo = new BasicDBObject(); bo.put('_id', 1); bo.put('name', 'bruce'); collection.insert(bo); 權限 判斷是否有mongoDB的訪問權限,有就返回true,否則返回false。 boolean auth = db.authenticate(myUserName, myPassword); 查看mongoDB數據庫列表 | Mongo m = new Mongo();
for (String s : m.getDatabaseNames()) {
System.out.println(s);
} | 查看當前庫下所有的表名,等于在mongoDB中執行showtables; | Set colls = db.getCollectionNames();
for (String s : colls) {
System.out.println(s);
} | 查看一個表的索引 | List list = coll.getIndexInfo();
for (DBObject o : list) {
System.out.println(o);
} | 刪除一個數據庫 | Mongo m = new Mongo();
m.dropDatabase("myDatabaseName"); | 建立mongoDB的鏈接 | Mongo m = new Mongo("localhost", 27017); //有多個重載方法,可根據需要選擇
DB db = m.getDB("myDatabaseName"); //相當于庫名
DBCollection coll = db.getCollection("myUsersTable");//相當于表名 | 查詢數據 查詢第一條記錄 DBObject firstDoc = coll.findOne(); findOne()返回一個記錄,而find()返回的是DBCursor游標對象。 查詢全部數據 | DBCursor cur = coll.find();
while(cur.hasNext()) {
System.out.println(cur.next());
} | 查詢記錄數量 | coll.find().count();
coll.find(new BasicDBObject("age", 26)).count(); | 條件查詢 | BasicDBObject condition = new BasicDBObject();
condition.put("name", "bruce");
condition.put("age", 26);
coll.find(condition); | 查詢部分數據塊 | DBCursor cursor = coll.find().skip(0).limit(10);
while(cursor.hasNext()) {
System.out.println(cursor.next());
} | 比較查詢(age >50) | BasicDBObject condition = new BasicDBObject();
condition.put("age", new BasicDBObject("$gt",50));
coll.find(condition); | 比較符 "$gt": 大于 "$gte":大于等于 "$lt": 小于 "$lte":小于等于 "$in": 包含 //以下條件查詢20<age<=30 condition.put("age", new BasicDBObject("$gt",20).append("$lte", 30)); 插入數據 批量插入 | List datas = new ArrayList();
for (int i=0; i < 100; i++) {
BasicDBObject bo = new BasicDBObject();
bo.put("name", "bruce");
bo.append("age", i);
datas.add(bo);
}
coll.insert(datas); | 又如: | DBCollection coll = db.getCollection("testCollection");
for(int i=1; i<=100; i++) {//插入100條記錄 User user = new User(); user.setName("user_"+i); user.setPoint(i); coll.insert(user);
} | 正則表達式 查詢所有名字匹配 /joh?n/i 的記錄 | Pattern pattern = Pattern.compile("joh?n",CASE_INSENSITIVE);
BasicDBObject query = new BasicDBObject("name", pattern);
DBCursor cursor = coll.find(query); | 15Redis簡介 Redis是一個key-value類型的內存數據庫,每一個key都與一個value關聯,使得Redis與其他key-value數據庫不同是因為在Redis中的每一個value都有一個類型(type),目前在Redis中支持5中數據類型:String、List、Set、ZSet和Hash。每一種類型決定了可以賦予其上的操作(這些操作成為命令command)。比如你可以使用LPUSH或RPUSH命令在O(1)時間對一個list添加一個元素,然后你可以使用LRANGE命令得到list中的一部分元素或使用LTRIM對該list進行trim操作。集合set操作也是很靈活的,你可以從set(無序的String的集合)中add或remove元素,還可以進行交集、合集和差集運算。每一個command都是服務端自動的操作。 Redis性能上和memcached一樣快但提供了更多的特性。和memcached一樣,Redis支持對key設置失效時間,因此當設定的時間過后會被自動刪除。 15.1Redis特性 速度快 Redis使用標準C編寫實現,而且將所有數據加載到內存中,所以速度非常快。官方提供的數據表明,在一個普通的Linux機器上,Redis讀寫速度分別達到81000/s和110000/s。 持久化 由于所有數據保持在內存中(2.0版本開始可以只將部分數據的value放在內存,見“虛擬內存”),所以對數據的更新將異步地保存到磁盤上,Redis提供了一些策略來保存數據,比如根據時間或更新次數。 數據結構 可以將Redis看做“數據結構服務器”。目前,Redis支持5種數據結構。 自動操作 Redis對不同數據類型的操作是自動的,因此設置或增加key值,從一個集合中增加或刪除一個元素都能安全的操作。 支持多種語言 Redis支持多種語言,諸如Ruby,Python, Twisted Python, PHP, Erlang, Tcl, Perl, Lua, Java, Scala, Clojure等。對Java的支持,包括兩個。一是JDBC-Redis,是使用JDBC連接Redis數據庫的驅動。這個項目的目標并不是完全實現JDBC規范,因為Redis不是關系型數據庫,但給Java開發人員提供了一個像操作關系數據庫一樣操作Redis數據庫的接口。 另一個是JRedis,是使用Redis作為數據庫開發Java程序的開發包,主要提供了對數據結構的操作。 主-從復制 Redis支持簡單而快速的主-從復制。官方提供了一個數據,Slave在21秒即完成了對Amazon網站10Gkey set的復制。 Sharding 很容易將數據分布到多個Redis實例中,但這主要看該語言是否支持。目前支持Sharding功能的語言只有PHP、Ruby和Scala。 16Redis數據類型 官方文檔上,將Redis成為一個“數據結構服務器”(data structures server)是有一定道理的。Redis的所有功能就是以其固有的幾種數據結構保存,并提供用戶操作這幾種結構的接口。可以對比在其他語言中那些固有數據類型及其操作。 Redis目前提供四種數據類型:string、list、set和zset(sortedset)。 16.1String類型 String是最簡單的類型,一個key對應一個value。RedisString是安全的,String類型的數據最大1G。String類型的值可以被視作integer,從而可以讓“INCR”命令族操作,這種情況下,該integer的值限制在64位有符號數。 在list、set和zset中包含的獨立的元素類型都是RedisString類型。 在Redis中,String類型由sds.c庫定義,它被封裝成Redis對象。和Java中的對象一樣,Redis對象也是使用“引用”,因此當一個Redis String被多次使用時,Redis會盡量使用同一個String對象而不是多次分配。 從Redis 1.1版開始,String對象可以編碼成數字,因此這樣可以節省內存空間。 16.2List類型 鏈表類型,主要功能是push、pop、獲取一個范圍的所有值等。其中的key可以理解為鏈表的名字。在Redis中,list就是RedisString的列表,按照插入順序排序。比如使用LPUSH命令在list頭插入一個元素,使用RPUSH命令在list的尾插入一個元素。當這兩個命令之一作用于一個空的key時,一個新的list就創建出來了。比如: 最終在mylist中存儲的元素為:”b”,”a”,”c”。 List的最大長度是2^32-1個元素。 16.3Set類型 集合,和數學中的集合概念相似。操作中的key理解為集合的名字。在Redis中,set就是RedisString的無序集合,不允許有重復元素。對set操作的command一般都有返回值標識所操作的元素是否已經存在。比如SADD命令是往set中插入一個元素,如果set中已存在該元素,則命令返回0,否則返回1。 Set的最大元素數是2^32-1。 Redis中對set的操作還有交集、并集、差集等。 16.4ZSet類型 Zset是set的一個升級版本,在set的基礎上增加了一個順序屬性,這一屬性在添加修改元素時可以指定,每次指定后zset會自動安裝指定值重新調整順序。可以理解為一張表,一列存value,一列存順序。操作中的key理解為zset的名字。 比如ZADD命令是向zset中添加一個新元素,對該元素要指定一個score。如果對已經在zset中存在的元素施加ZADD命令同時指定了不同的score,則該元素的score將更新同時該元素將被移動到合適的位置以使集合保持有序。 使用ZRANGE命令可以得到zset的一部分元素,當然也可以使用命令ZRANGEBYSCORE,根據score得到或刪除一部分元素。 Zset的最大元素數是2^32-1。 對于已經有序的zset,仍然可以使用SORT命令,通過指定ASC|DESC參數對其進行排序。 16.5Hash類型 Redis Hash類型對數據域和值提供了映射,這一結構很方便表示對象。此外,在Hash中可以只保存有限的幾個“域”,而不是將所有的“域”作為key,這可以節省內存。這一特性的體現可以參考下文的“虛擬內存”部分。 17Alldata in memory, but saved on disk Redis在內存中加載并維護整個數據集,但這些數據是持久化的,因為它們在同一時間被保持在磁盤上,所以當Redis服務重啟時,這些數據能夠重新被加載到內存中。 Redis支持兩種數據持久化的方法。一種稱為snapsshotting,在這種模式下,Redis異步地將數據dump到磁盤上。Redis還可以通過配置,根據更新操作次數或間隔時間,將數據dump到磁盤。比如你可以設定發生1000個更新操作時,或距上次轉儲最多60s就要將數據dump到磁盤。 因為數據是異步dump的,所以當系統崩潰時,就會出現錯誤。因為,Redis提供了另外一種模式,更安全的持久化模式,稱為AppendOnly File,當出現修改數據命令的地方,這些命令就要寫到“appendonly file”—ASAP。當服務重啟時,這些命令會重新執行(replay)從而在內存中重新構建數據。 Redis的存儲可以分為:內存存儲、磁盤存儲和log文件三部分,配置文件(redis.conf)中有三個參數對其進行配置。 save<seconds> <changes>:save配置,指出在多長時間,有多少次更新操作,就將數據同步到數據文件。在默認的配置文件中設置就設置了三個條件。 appendonlyyes/no:appendonly配置,指出是否在每次更新操作后進行日志記錄,如果不開啟,可能會在斷電時導致一段時間內的數據丟失。因為redis本身同步數據文件是按上面的save條件來同步的,所以有的數據會在一段時間內只存在于內存中。 appendfsyncno/always/everysec:appendfsync配置,no表示等操作系統進行數據緩存同步到磁盤,always表示每次更新操作后手動調用fsync()將數據寫到磁盤,everysec表示每秒同步一次。 18Redis的Master-Slave模式 Redis中配置Master-Slave模式很簡單,二者會自動同步。配置方法是在從機的配置文件中指定slaveof參數為主機的ip和port即可,比如:slaveof 192.168.2.2016379。從原理上來說,是從機請求主機的方式,按照tokyotyrant的做法,是可以實現主-主,主-從等靈活形式的,而redis只能支持主-從,不能支持主-主。Redis中的復制具有以下特點: - 一個master可以有多個slave。
- 一個slave可以接收其他slave的鏈接,即不僅僅能連接一個master所屬的slaves,而且能連接到slave的slave,從而形成了“圖”結構。
- 在復制進行時,master是“非阻塞”的。即一個或多個slave對master進行第一次同步時,master仍然可以提供查詢服務,而對于slave,當復制進行時則是“阻塞”的,期間slave不能響應查詢。
- 復制可以用于提高數據的擴展性(比如多個slave用于只讀查詢),或者僅用于數據備份。
- 使用復制可以避免在master存儲數據,通過修改redis.conf文件(將“save”的al注釋),這樣數據存儲只在slave端進行。
slave連接master并發出SYNC命令開始復制或當連接關閉時與master同步數據。master在“后臺”進行存儲操作,同時會監聽所有對數據的更新操作。當存儲操作完成后,master開始向slave傳送數據并將數據保存到磁盤,加載到內存。這一過程中,master將會向slave發送所有對數據有更新操作的命令。通過telnet可以實驗這一過程。通過連接到Redis端口并且發出SYNC命令,在telnet的session中,可以看到數據包的傳送,以及發送給master的命令都將重新發送給slave。 當master-slave連接斷掉時,slave能自動重新建立連接。當一個master并發地收到多個slave數據同步請求時,master也能自動處理。 19Redis虛擬內存管理 在Redis2.0開始(目前最新版本)第一次提出了Virtual Memory(VM)的特性。Redis是內存數據庫,因此通常情況下Redis會將所有的key和value都放在內存中,但有時這并不是最好的選擇,為了查詢速度,可以將所有的key放在內存中,而values可以放在磁盤上,當用到時再交換到內存。 比如你的數據有100000個key都放在了內存中,而只有其中10%的key被經常訪問,那么可進行VM配置的Redis會嘗試將不經常訪問的key關聯的value放到磁盤上,當這些value被請求時才會交換到內存中。 什么時候使用VM配置是需要考慮的問題。因為Redis是以磁盤為后備的內存數據庫,在大多數情況下,只有RAM足夠大時才使用Redis。但實際情況是,總會出現內存不夠的情況: - 有“傾向”的數據訪問。只有很小比例的key被經常訪問(比如一個網站的在線用戶),但同時在內存中卻有所有的數據。
- 不考慮訪問情況以及大的value,內存不能加載所有數據。這種情況下,Redis可以視作on-disk DB,即所有的key在內存中,而訪問value時要訪問慢速的磁盤。
需要注意的一點是,Redis不能交換key。即如果是因為數據的key太大,value太小而造成內存不夠,VM是不能解決這種情況的。當出現這種情況時,有時可以使用Hash結構,將“manykeys with small values”的問題轉換成“less keys but withvery values”的問題(對key進行hash,從而對數據進行“分組”)。 啟用VM功能,只需要在redis.conf文件中使vm-enabled的值為yes。 20Redis實例分析 仍然以用戶和圖片的例子進行說明。假設將圖片存儲在文件系統中,即類Image中只保存圖片的uri。類Image和類User的代碼參見MongoDB部分的例子。下面給出測試類的關鍵代碼: 在實現中,將類User的對象實例存放在一個set中,所以利用了SADD命令,在Java中即調用JRedis對象的sadd方法。smembers方法將返回指定key(這里是userSet)的集合中的所有value,這樣就可以對每一個元素進行操作。 21Redis命令總結 Redis提供了豐富的命令(command)對數據庫和各種數據類型進行操作,這些command可以在Linux終端使用。在編程時,比如使用Redis 的Java語言包,這些命令都有對應的方法,比如上面例子中使用的sadd方法,就是對集合操作中的SADD命令。下面將Redis提供的命令做一總結。 21.1連接操作相關的命令 - quit:關閉連接(connection)
- auth:簡單密碼認證
21.2對value操作的命令 - exists(key):確認一個key是否存在
- del(key):刪除一個key
- type(key):返回值的類型
- keys(pattern):返回滿足給定pattern的所有key
- randomkey:隨機返回key空間的一個key
- rename(oldname, newname):將key由oldname重命名為newname,若newname存在則刪除newname表示的key
- dbsize:返回當前數據庫中key的數目
- expire:設定一個key的活動時間(s)
- ttl:獲得一個key的活動時間
- select(index):按索引查詢
- move(key, dbindex):將當前數據庫中的key轉移到有dbindex索引的數據庫
- flushdb:刪除當前選擇數據庫中的所有key
- flushall:刪除所有數據庫中的所有key
21.3對String操作的命令 - set(key, value):給數據庫中名稱為key的string賦予值value
- get(key):返回數據庫中名稱為key的string的value
- getset(key, value):給名稱為key的string賦予上一次的value
- mget(key1, key2,…, key N):返回庫中多個string(它們的名稱為key1,key2…)的value
- setnx(key, value):如果不存在名稱為key的string,則向庫中添加string,名稱為key,值為value
- setex(key, time,value):向庫中添加string(名稱為key,值為value)同時,設定過期時間time
- mset(key1, value1, key2, value2,…key N, value N):同時給多個string賦值,名稱為keyi的string賦值valuei
- msetnx(key1, value1, key2, value2,…key N, value N):如果所有名稱為keyi的string都不存在,則向庫中添加string,名稱keyi賦值為value i
- incr(key):名稱為key的string增1操作
- incrby(key, integer):名稱為key的string增加integer
- decr(key):名稱為key的string減1操作
- decrby(key, integer):名稱為key的string減少integer
- append(key, value):名稱為key的string的值附加value
- substr(key, start, end):返回名稱為key的string的value的子串
21.4對List操作的命令 - rpush(key, value):在名稱為key的list尾添加一個值為value的元素
- lpush(key, value):在名稱為key的list頭添加一個值為value的元素
- llen(key):返回名稱為key的list的長度
- lrange(key, start, end):返回名稱為key的list中start至end之間的元素(下標從0開始,下同)
- ltrim(key, start, end):截取名稱為key的list,保留start至end之間的元素
- lindex(key, index):返回名稱為key的list中index位置的元素
- lset(key, index, value):給名稱為key的list中index位置的元素賦值為value
- lrem(key, count, value):刪除count個名稱為key的list中值為value的元素。count為0,刪除所有值為value的元素,count>0從頭至尾刪除count個值為value的元素,count<0從尾到頭刪除|count|個值為value的元素。
- lpop(key):返回并刪除名稱為key的list中的首元素
- rpop(key):返回并刪除名稱為key的list中的尾元素
- blpop(key1, key2,… key N, timeout):lpop命令的block版本。即當timeout為0時,若遇到名稱為keyi的list不存在或該list為空,則命令結束。如果timeout>0,則遇到上述情況時,等待timeout秒,如果問題沒有解決,則對key i+1開始的list執行pop操作。
- brpop(key1, key2,… key N, timeout):rpop的block版本。參考上一命令。
- rpoplpush(srckey, dstkey):返回并刪除名稱為srckey的list的尾元素,并將該元素添加到名稱為dstkey的list的頭部
21.5對Set操作的命令 - sadd(key, member):向名稱為key的set中添加元素member
- srem(key, member) :刪除名稱為key的set中的元素member
- spop(key) :隨機返回并刪除名稱為key的set中一個元素
- smove(srckey, dstkey, member) :將member元素從名稱為srckey的集合移到名稱為dstkey的集合
- scard(key) :返回名稱為key的set的基數
- sismember(key, member) :測試member是否是名稱為key的set的元素
- sinter(key1, key2,…key N) :求交集
- sinterstore(dstkey, key1, key2,…key N) :求交集并將交集保存到dstkey的集合
- sunion(key1, key2,…key N) :求并集
- sunionstore(dstkey, key1, key2,…key N) :求并集并將并集保存到dstkey的集合
- sdiff(key1, key2,…key N) :求差集
- sdiffstore(dstkey, key1, key2,…key N) :求差集并將差集保存到dstkey的集合
- smembers(key) :返回名稱為key的set的所有元素
- srandmember(key) :隨機返回名稱為key的set的一個元素
21.6對zset(sorted set)操作的命令 - zadd(key, score, member):向名稱為key的zset中添加元素member,score用于排序。如果該元素已經存在,則根據score更新該元素的順序。
- zrem(key, member) :刪除名稱為key的zset中的元素member
- zincrby(key, increment, member) :如果在名稱為key的zset中已經存在元素member,則該元素的score增加increment;否則向集合中添加該元素,其score的值為increment
- zrank(key, member) :返回名稱為key的zset(元素已按score從小到大排序)中member元素的rank(即index,從0開始),若沒有member元素,返回“nil”
- zrevrank(key, member) :返回名稱為key的zset(元素已按score從大到小排序)中member元素的rank(即index,從0開始),若沒有member元素,返回“nil”
- zrange(key, start, end):返回名稱為key的zset(元素已按score從小到大排序)中的index從start到end的所有元素
- zrevrange(key, start, end):返回名稱為key的zset(元素已按score從大到小排序)中的index從start到end的所有元素
- zrangebyscore(key, min, max):返回名稱為key的zset中score>= min且score <= max的所有元素
- zcard(key):返回名稱為key的zset的基數
- zscore(key, element):返回名稱為key的zset中元素element的score
- zremrangebyrank(key, min, max):刪除名稱為key的zset中rank>= min且rank <= max的所有元素
- zremrangebyscore(key, min, max) :刪除名稱為key的zset中score>= min且score <= max的所有元素
- zunionstore/ zinterstore(dstkeyN, key1,…,keyN, WEIGHTS w1,…wN, AGGREGATE SUM|MIN|MAX):對N個zset求并集和交集,并將最后的集合保存在dstkeyN中。對于集合中每一個元素的score,在進行AGGREGATE運算前,都要乘以對于的WEIGHT參數。如果沒有提供WEIGHT,默認為1。默認的AGGREGATE是SUM,即結果集合中元素的score是所有集合對應元素進行SUM運算的值,而MIN和MAX是指,結果集合中元素的score是所有集合對應元素中最小值和最大值。
21.7對Hash操作的命令 - hset(key, field, value):向名稱為key的hash中添加元素field<—>value
- hget(key, field):返回名稱為key的hash中field對應的value
- hmget(key, field1, …,field N):返回名稱為key的hash中fieldi對應的value
- hmset(key, field1, value1,…,field N, value N):向名稱為key的hash中添加元素fieldi<—>value i
- hincrby(key, field, integer):將名稱為key的hash中field的value增加integer
- hexists(key, field):名稱為key的hash中是否存在鍵為field的域
- hdel(key, field):刪除名稱為key的hash中鍵為field的域
- hlen(key):返回名稱為key的hash中元素個數
- hkeys(key):返回名稱為key的hash中所有鍵
- hvals(key):返回名稱為key的hash中所有鍵對應的value
- hgetall(key):返回名稱為key的hash中所有的鍵(field)及其對應的value
21.8持久化 - save:將數據同步保存到磁盤
- bgsave:將數據異步保存到磁盤
- lastsave:返回上次成功將數據保存到磁盤的Unix時戳
- shundown:將數據同步保存到磁盤,然后關閉服務
21.9遠程服務控制 - info:提供服務器的信息和統計
- monitor:實時轉儲收到的請求
- slaveof:改變復制策略設置
- config:在運行時配置Redis服務器
|