我最近一直在研究MongoDB,有些小心得。恰好發現原來部落格園支援Live writer啊
興奮異常,終於多年以後重回這裡。以前一直用liver writer寫 myspace和 wordpress
但是前者完了,後者翻牆很煩。
====================================================
首先推薦一個MongoDB的查詢分析器
MongoVUE
這個工具是非常好用,雖然超過試用期,但是仍然可以使用
只是只能開三個查詢時段而已。
以前一直使用db4o, protobuf.net ,所以對mongoDB還是很適應的。
因為相似性太大。尤其是對象持久化的方式,細節略微不同而已。
=============================================
1.需求:
我的一個新寫的演算法需要讀取一個完整的collection,而這需要幾十秒鐘。
而一開始都是使用特性標註的自動序列化和還原序列化,無論用任何方式調整,InsertBatch和
FindAll() 的效能都得不到提高。
2.思考:
我一開始以為讀取速度和儲存奇慢無比,是因為mongoDB自己的問題。今天仔細想了想。問題關鍵在於寫入硬碟的資料太多。
mongoDB的資料持久化是以BSON格式的。而這種格式的冗餘還是相當大的。尤其是預設序列化和還原序列化。
"_id" : ObjectId("4f4e2a02c992571e54c30465"),
"value" : "xxxxx",
"chars" : [{
"words" : [{
"index" : 0,
"length" : 2,
"wordTypes" : 0
}]
}, {
"words" : [{
"index" : 0,
"length" : 2,
"wordTypes" : 0
}, {
"index" : 1,
"length" : 2,
"wordTypes" : 0
}]
},
用mongoVUE查看最終資料格式,發覺主要儲存空間消耗在意義不大的屬性name上。計算一下就可以知道,名稱幾乎是值的5-10倍空間大小。
相比 protobuf,採用數字作為屬性的名稱,就十分節省空間的了。
但是mongodb可以檢索欄位,而protobuf不可以,所以mongo沒有採用protobuf的方式。
我有一個collection有50000個document,平均一個document 4000byte,這真是令人吃驚的低效持久化啊。怪不得讀取都需要幾十秒鐘。整個資料存放區消耗了200m空間。
由於看過mongoDB的官方文檔
http://www.mongodb.org/display/DOCS/CSharp+Language+Center
所以對Customizing serialization有點印象。
官方文檔描述十分簡略,只說了應該將類繼承IBsonSerializable 介面,然後實現四個方法。但是沒有樣本,完全不知道如何具體操作。
public class MyClass : IBsonSerializable { // implement Deserialize method // implement Serialize method }
好吧有google大神在。
stackoverflow是個好網站
http://stackoverflow.com/questions/7105274/storing-composite-nested-object-graph
3.解決:
第一部分:將對象變換成數字,節省名稱和空間消耗
public UInt32 IntValue
{
get
{
var v1 = ((UInt32)WordTypes) << 24;
var v2 = ((UInt32)Index) << 16;
var v3 = ((UInt32)Length) << 8;
var v4 = (UInt32)0;//預留
return v1 | v2 | v3 | v4;
}
}
public void FromInt32(UInt32 value)
{
this.WordTypes = (WordTypes)(value >> 24);
this.Index = (Int32)(value<<8 >> 24);
this.Length = (Int32)(value << 16 >> 24);
}
以上沒什麼好講的,無非左移右移,當然可能會出現資料類型溢出可能,如果有這種情況,換成Int64,或者適當修改。
說明一下,這個三級對象我不準備在mongoDB中檢索欄位,而是只用於儲存,至於檢索是變換成另外字串keyword的方式來檢索。
所以既然不需要檢索,屬性也就根本不需要有name,所以多個屬性可以位或成一個數值,存放到數組中。對象都省了。
第二部分
public partial class Sentence : IBsonSerializable
{
public static int idSum;
public bool GetDocumentId(out object id, out Type idNominalType, out IIdGenerator idGenerator)
{
id = this.Id = idSum++;
idNominalType = typeof(int);
idGenerator = null;
return true;
}
public void Serialize(MongoDB.Bson.IO.BsonWriter bsonWriter, Type nominalType, IBsonSerializationOptions options)
{
bsonWriter.WriteStartDocument();
bsonWriter.WriteInt32("_id", this.Id); //10多個個位元組,如果用objectId
bsonWriter.WriteString("value", this.Value);//名稱如果都改用幾個字母可以節省十幾個個位元組
bsonWriter.WriteString("words", this.WordStr);
bsonWriter.WriteBoolean("isConf", this.IsConflict);
bsonWriter.WriteStartArray("c");
foreach (var item in Chars)
{
BsonSerializer.Serialize(bsonWriter, item.Words.Select(v=>v.IntValue).ToList());
}
bsonWriter.WriteEndArray();
bsonWriter.WriteEndDocument();
}
public void SetDocumentId(object id)
{
throw new NotImplementedException();
}
public object Deserialize(MongoDB.Bson.IO.BsonReader bsonReader, Type nominalType, IBsonSerializationOptions options)
{
//bsonReader.ReadStartDocument();
//this.Id = bsonReader.ReadInt32();
//var value=bsonReader.ReadString("v");
//var wordStr=bsonReader.ReadString("w");
//bsonReader.ReadStartArray();
//var list = new List<List<Int32>>();
//while (bsonReader.ReadBsonType() != BsonType.EndOfDocument)
//{
// var element = BsonSerializer.Deserialize<List<Int32>>(bsonReader);
// list.Add(element);
//}
//bsonReader.ReadEndArray();
//var isConflict=bsonReader.ReadBoolean("i");
//bsonReader.ReadEndDocument();
if (nominalType != typeof(Sentence))
throw new ArgumentException("不能序列化,因為類型定義不一致");
var doc = BsonDocument.ReadFrom(bsonReader);
this.Id = (Int32)doc["_id"];
this.Value = (string)doc["value"];
this.WordStr = (string)doc["words"];
this.IsConflict = (bool)doc["isConf"];
var list = (BsonArray)doc["c"];
this.Chars = new List<CharObj>();
for (int i = 0; i < list.Count; i++)
{
var ch = new CharObj { Index = i, Sen = this, Words=new List<WordObj>() };
this.Chars.Add(ch);
var words = (BsonArray)list[i];
foreach (Int32 item in words)
{
var wordObj = new WordObj((UInt32)item);
wordObj.Sen = this;
ch.Words.Add(wordObj);
}
}
return this;
//return new Sentence { Id=1, IsConflict= true, Value="1", WordStr= "1"};
}
}
主要有幾個注意地方:
一個是Id的產生。我有點不明白為什麼id賦值函數要弄的那麼複雜的參數,但是這樣可以繞過ObjectID的 guid式的id,使用int可以節省一些空間。
當然,如果整體對象比較大,還是用objectID吧。完全沒必要用int,int也有很多問題,需要儲存最大值在另外的collection,沒法像ObjectId一樣跨多個Collection。所以mongoDB設計Id 用ObjectId而不是int,是非常有道理的。如果對象整體比較大,還是沒必要節省這十幾個位元組的消耗。
二是Serialize 方法的實現中,必須要以bsonWriter.WriteStartDocument()開始 bsonWriter.WriteEndDocument() 結束,切記,否則會報出一個沒法write的錯誤。
三是如何對二層的集合進行寫入,我原來是這樣寫的
foreach (var item in Chars)
{
bsonWriter.WriteStartArray("words");
foreach (var w in item.Words)
bsonWriter.WriteInt32((Int32)w.IntValue);
bsonWriter.WriteEndArray();
}
但是mongoDB不支援這種嵌套式的持久化。
必須改成
foreach (var item in Chars)
{
BsonSerializer.Serialize(bsonWriter, item.Words.Select(v=>v.IntValue).ToList());
}
那個注意雖然 BsonSerializer.Serialize的參數是一個IEnumerable<T> 但是必須要ToList,否則不會儲存成功資料
第四,還原序列化的時候不能直接用start end方式,必然會報錯,只能先一次讀取,再取字典值
4.對比
新的bson格式的儲存比較緊湊了。
"_id" : ObjectId("4f4e2a02c992571e54c30465"),
"value" : "xxxxx",
"chars" : [{
"words" : [{
"index" : 0,
"length" : 2,
"wordTypes" : 0
}]
}, {
"words" : [{
"index" : 0,
"length" : 2,
"wordTypes" : 0
}, {
"index" : 1,
"length" : 2,
"wordTypes" : 0
}]
},
對比原來的,差距非常明顯。
用mongoVUE 查看平均 document大小,平均只有364byte了。原來可是嚇死人的4000
而合計Size也從200m下降到17m
而耗時 用我筆記本,耗時大概9秒鐘。原來40秒以上。而用台式機硬碟快,可以快幾倍,幾秒鐘內搞定。
5.其他
其實為什麼要實現自訂的持久化方法,一當然是效能十分的讓人憂慮。第二個則是對象關聯指標的重新綁定問題。
原來從資料庫讀取的資料,需要手工恢複相互關聯的指標,現在可以在還原序列化函數中直接完成這個操作。
也就是說,一旦查詢出來的對象,都已經和記憶體對像一摸一樣了。
好處是大大降低了程式的複雜度。
使用mongoDB資料對象,猶如記憶體對象一樣進行指標操作。然後自動永久化資料。
呃。我發覺愛上mongoDB了。雖然它還有不少缺點。