很早以前收藏了一片文章:《強大的MongoDB數據庫管理工具》。最近剛好要做一些MongoDB方面的工作,於是翻出來溫習了一下,用起來也確實挺方便。不過在使用過程中出現了一些個問題,加上更喜歡MongoUVE的操作習慣,於是決定“自己動手,豐衣足食”,寫一個升級版的工具。
- 原版是用的WebForm開發的,新版打算升級到MVC
- 前端框架使用bootstrap
- Mongodb的驅動程序改為使用官方版的驅動程序
- 原版查詢數據時使用的是解析sql語句的方式,新版打算采用javascript shell命令
- 原有的功能點都進行一些輔助性的改進
下面在對各個功能介紹的過程中,對升級的細節進行逐一的介紹,改動的地方會增加下划線進行標注。
系統主界面如下:
主界面采用了bootstrap的柵格系統進行布局:頂部為banner導航,左側為Mongo對象的樹菜單區域,右側為功能操作的主區域。點擊頂部banner和左側上部的刷新圖標均能重新加載主界面。主區域默認進入服務器管理的界面。

<div class="row-fluid">
<div class="span3 site-left">
<div class="form-inline site-left-top">
<i class="icon-list"></i> <span>
<a href="/Home/ServerManager/" target="mainFrame">Servers</a></span>
<a action="refresh"><i class="icon-refresh"></i></a>
<a action="expand"><i class="icon-chevron-down"></i></a>
<a action="collapse"><i class=" icon-chevron-up"></i></a>
</div>
<div id="menu">
<ul id="tree" class="ztree">
</ul>
</div>
</div>
<div class="span9 pr10">
<iframe id="mainFrame" name="mainFrame" height="100%" width="100%" frameborder="0" src="/Home/ServerManager/"></iframe>
</div>
</div>
本例中,樹菜單里的測試服務器有兩個,是在本機通過不同端口建立的兩個MongoDB實例,用來進行主從庫的模擬(這個在后面會用到)。
首先,樹菜單采用了zTree控件代替了原來的TreeView控件。zTree是一個非常好用的樹控件,它支持json數據綁定,支持異步加載,對節點的控制和操作都非常強大,甚至可以直接在樹節點上編輯數據。個人非常推薦這個控件。點擊這里可以進入zTree控件的官網。
其次,Mongo對象的采用了“服務器 》數據庫 》 數據表 》 字段和索引”的層次結構。因為字段和索引都處在表節點的下級,因此加入了兩個虛擬節點【表信息】和【索引】來進行分類組織。並且點擊不同的對象節點會觸發不同的操作。
按照MongoUVE的表現形式,表節點右邊增加了該表的數據總量的顯示,字段節點右邊增加了該字段的數據類型的顯示。具體界面如下圖:
系統定義了樹節點的數據結構:MongoTreeNode,用來生成綁定樹控件的json數據。每個節點對應的對象類型也定義了對應的枚舉:MongoTreeNodeType。代碼如下:

/// <summary>
/// 樹節點 /// </summary>
public class MongoTreeNode { /// <summary>
/// 唯一值 /// </summary>
public uint ID { get; set; } /// <summary>
/// 父節點ID /// </summary>
public uint PID { get; set; } /// <summary>
/// 名稱 /// </summary>
public string Name { get; set; } /// <summary>
/// 類型 /// </summary>
public MongoTreeNodeType Type { get; set; } } /// <summary>
/// 節點類型 /// </summary>
public enum MongoTreeNodeType { /// <summary>
/// 服務器 /// </summary>
[Description("服務器")] Server = 1, /// <summary>
/// 數據庫 /// </summary>
[Description("數據庫")] Database = 2, /// <summary>
/// 數據表 /// </summary>
[Description("數據表")] Collection = 3, /// <summary>
/// 字段 /// </summary>
[Description("字段")] Field = 4, /// <summary>
/// 索引 /// </summary>
[Description("索引")] Index =5, /// <summary>
/// 表信息填充節點 /// </summary>
[Description("表信息填充節點")] TableFiller = 6, /// <summary>
/// 索引填充節點 /// </summary>
[Description("索引填充節點")] IndexFiller = 7 }
zTree控件在綁定之前需要對各種屬性進行預定義,詳情請參考zTree控件的API手冊。在本系統中預制了ID和PID字段作為父子關系綁定的字段,而控件默認值是id和pId字段。

var setting = { view: { dblClickExpand: false, showLine: true, selectedMulti: false }, data: { key: { name: "Name" }, simpleData: { enable: true, idKey: "ID", pIdKey: "PID", rootPId: "" } }, callback: { onClick: nodeClick } };
*注意:在實際使用的過程中,在調用樹控件的綁定方法時,偶爾還會報“找不到綁定字段ID”的錯,導致樹控件無法顯示。這個問題有待解決。
系統還定義了Mongo對象的數據結構,主要有“服務器、數據庫、數據表 、字段和索引”五種(具體見代碼)。對象的ID字段分別對應於樹節點的ID和PID字段,即數據庫對象的ID,既對應數據庫節點的ID字段,又對應表節點的PID字段。對這些對象的結構定義,比原版系統精簡了很多。
在讀取數據表對象時,過濾了一些系統表和有特殊意義的表(如索引表,profile數據表)。如果需要放開顯示的話,可以去程序中進行屏蔽。代碼如下:

collections.Where(t => !t.Contains("$") && !t.Contains(MongoConst.IndexTableName) && !t.Contains(MongoConst.ProfileTableName)).ToList().ForEach(t => { ... }
有一個改進是:對象ID字段的數據類型從原來的GUID改為uint類型(為了保證數值大於0,切避免出現正值和負值取絕對值后重復的可能性,才采用無符合數),以提高查找效率並減小json數據的大小。ID字段是由程序自動生成的唯一的隨機值,它是在一個並行計算(之后會講到)過程中得到的。為了保證在並行環境中隨機數的唯一性,參考了《如何在高並發分布式系統中生成全局唯一Id》這篇文章。最后形成的代碼很簡單,經過測試到目前為止還沒有出現過ID值重復的情況。代碼如下:

/// <summary>
/// 隨機數的步長 /// </summary>
private static readonly int StepLength = 8; /// <summary>
/// 生成隨機數 /// </summary>
/// <returns></returns>
public static uint GetRandomId() { var bytes = new byte[StepLength]; var provider = new RNGCryptoServiceProvider(); provider.GetNonZeroBytes(bytes); return BitConverter.ToUInt32(bytes, 0); }
Mongo對象和樹節點是同一時間讀取的,在第一次加載完后就會保存至緩存中,緩存時間為2小時。當緩存超時后再點擊樹菜單節點或進行其他操作時,在主區域都會出現如下的界面,要求重新加載數據:
上面提到的並行計算的方法,是在每次從數據庫中加載數據時才會調用,這也是本次一大改進。通過采用並行計算,確實提高了讀取數據的效率,數據庫內對象越多越明顯。關於並行計算的學習,我推薦《8天玩轉並行開發》這一系列的文章,非常好。新版系統的代碼里保留了循環讀取Mongo對象的代碼,有興趣的朋友,可以自己修改一下程序去體會一下並行和循環的差別。為了保證在並行計算中的線程安全,樹節點采用HashSet進行緩存,HashSet本身是線程安全的;而Mongo對象則采用Hashtable進行緩存,在初始化時,需要使用Synchronized方法來保證線程安全。

public HashSet<MongoTreeNode> TreeNodes { get; set; } public Hashtable MongoObjects { get; set; } public MongoContext() { TreeNodes = new HashSet<MongoTreeNode>(); MongoObjects = Hashtable.Synchronized(new Hashtable()); GetServer(); }
服務器管理界面在初始加載時就會顯示在主區域內。通過點擊左側頂部的【Servers】鏈接,也能操作服務器管理。主區域的操作界面,統一采用了bootstrap中的折疊插件來進行展示,點擊不同的折疊欄,會顯示該欄的內容同時折疊其他的內容。
服務器管理功能,實質是修改程序里【Config\servers.config】的文件,為其添加或刪除節點。文件里保存的數據,作為並行計算讀取數據的起點。
點擊樹菜單中的服務器節點,就會在主區域內顯示該服務器的統計信息。在主區域的頂部,會顯示服務器對象的一些基本信息。本系統中,大量的信息展示均使用zTree控件進行展現,因此重用了MongoTreeNode的結構,不同的地方僅在於不需要為MongoTreeNodeType屬性賦值。
查看服務器的統計信息,實際對應於MongoDB的adminCommand命令:db.adminCommand{"serverStatus", 1}。新版系統實現如下:

public override List<MongoTreeNode> GetInfo() { var mongo = new MongoClient(string.Format(MongoConst.ConnString, Server.Name)); var server = mongo.GetServer(); var adminDB = server.GetDatabase(MongoConst.AdminDBName); var rst = adminDB.RunCommand(new CommandDocument { { "serverStatus", 1 } }); var list = new List<MongoTreeNode>(); if (rst.Ok) { BuildTreeNode(list, 0, rst.Response); } return list; }
這里簡單介紹一下MongoDB的BsonDocument。詳情請移步至官方文檔: C# and .Net Mongo Driver。
BsonDocument是官方驅動進行各種操作的一個基礎數據,它是一個鍵/值對的集合,有三種生成方式:
- 使用Add或者Set方法
var book = new BsonDocument(); book.Add("author", "Ernest Hemingway"); book.Add("title", "For Whom the Bell Tolls");
- 使用Add或者Set的流式接口方法
var book = new BsonDocument().Add("author", "Ernest Hemingway").Add("title", "For Whom the Bell Tolls");
- 使用C#的類初始化語法(推薦使用此種方法)
var book = new BsonDocument { { "author", "Ernest Hemingway" }, { "title", "For Whom the Bell Tolls" } };
基於BsonDocument派生出很多子類,包括:
- CommandDocument:用於運行命令的參數文檔,在RunCommand方法中使用
- FieldsDocument:用於查詢返回字段的參數文檔,在SetFields方法中使用
- GroupByDocument:用於group的參數文檔(這個的用處暫時沒體會得到)
- QueryDocument:用於設置查詢條件的參數文檔,在Find方法中使用
- SortByDocument:用於設置排序規則的參數文檔,在SetSortOrder方法中使用
- UpdateDocument:用於修改數據的參數文檔,在Update方法中使用
其他子類和它們的具體使用方法,需要在使用過程中去體會。一般來說,什么樣的類型就對應什么樣的操作方法。在方法調用的參數接口說明中,都會有比較明顯的標識。
在文章的第一節說明了在本機搭建有主從環境,其中127.0.0.1:27017是主服務器,其中127.0.0.1:28018是從服務器。通過點擊【查看同步信息】鏈接,可以進入查看同步信息的頁面。
主服務器信息:
主服務器的同步日志,新版系統設定為僅顯示10條記錄,可以自己進行修改:
從服務器信息:
從服務器的源信息:
服務器的主從信息也都是使用adminCommand命令來獲取。主服務器的同步日志則是從local庫中的【oplog.$main】表中讀取,從服務器的源信息則是從local庫中的【sources】表讀取。實現代碼:

//服務器的主從信息
var stats = adminDB.RunCommand(new CommandDocument { { "ismaster", 1 } }); var localDB = server.GetDatabase(MongoConst.LocalDBName); //主服務器的同步日志
var docs = localDB.GetCollection(MongoConst.OplogTableName).FindAll().SetLimit(10); //從服務器的源信息
var doc = localDB.GetCollection(MongoConst.SourceTableName).FindOne();
點擊樹菜單中的數據庫節點,就會在主區域內顯示該數據庫的統計信息。在主區域的頂部,會顯示數據庫對象的一些基本信息。
數據庫的統計信息是使用RunCommand命令來獲取,因為是查看某個具體數據庫的統計信息,因此不需要使用Admin庫,即不使用adminCommand。代碼如下:

public override List<MongoTreeNode> GetInfo() { var mongo = new MongoClient(string.Format(MongoConst.ConnString, Server.Name)); var server = mongo.GetServer(); var db = server.GetDatabase(Database.Name); var rst = db.RunCommand(new CommandDocument { { "dbstats", 1 } }); var list = new List<MongoTreeNode>(); if (rst.Ok) { BuildTreeNode(list, 0, rst.Response); } return list; }
在數據庫的統計信息頁面點擊【Profile優化】的鏈接,即進入數據庫的Profile設置的界面。Profile的狀態一共有三種:
- 不開啟:即不記錄pofile數據,這樣就無法進行效率分析
- 記錄慢命令:僅記錄執行速度慢的profile數據,可以自行設置判斷是否是慢命令的執行時間的臨界值,一般推薦此種方式
- 記錄所有命令:記錄所有執行的profile數據,因為記錄profile數據也需要系統資源和開銷,因此不推薦開啟記錄所有命令的方式
查看Profile數據,可以自行調整顯示的條數。官方驅動返回的數據類型是 SystemProfileInfo,大家可以查看源代碼獲得詳細的信息。在本系統中選取了其中幾個屬性進行了一下轉換,並采用更直觀的列表形式來進行展示。
優化的一些小建議:
- 當【掃描記錄數】遠大於【返回記錄數】,可以考慮通過增加索引來優化查詢效率
- 當【返回結果集】過大,那么說明我們返回的結果集太大了,可以考慮只返回所需要的字段
- 如果寫查詢量或者update量過大的話,多加索引是會有好處的;執行 update 操作時同樣檢查一下【掃描記錄數】,並使用索引減少文檔掃描數量
- 如果讀操作很少,那么盡量不要添加索引,因為索引越多,寫操作會越慢;如果讀操作很多,則需要考慮創建合適的索引
點擊樹菜單中的數據表節點,就會在主區域內顯示該數據表的統計信息。在主區域的頂部,會顯示數據表對象的一些基本信息。
數據表的統計信息是使用RunCommand命令來獲取,需要在某個具體的數據庫上執行該命令,並需要傳入對應表的名稱。代碼如下:

public override List<MongoTreeNode> GetInfo() { var mongo = new MongoClient(string.Format(MongoConst.ConnString, Server.Name)); var server = mongo.GetServer(); var db = server.GetDatabase(Database.Name); var rst = db.RunCommand(new CommandDocument { { "collstats", Table.Name } }); var list = new List<MongoTreeNode>(); if (rst.Ok) { BuildTreeNode(list, 0, rst.Response); } return list; }
在數據表統計信息的頁面點擊【查看數據】鏈接,或者是點擊樹菜單中的【表信息】節點,均可以進入數據查看頁面。注意:【表信息】的子節點是字段節點,點此節點不做任何操作。
數據查詢的操作方式拋棄了原版系統解析sql語句的方式,采用了類似MongoUVE的操作形式,輸入json格式的命令進行查詢。新版系統可設置【find、sort、skip、limit】的參數。查詢結果采用列表方式展示,表頭內顯示字段名以及該字段的數據類型。在顯示數據時,當數據為BsonDocument類型時,做了序列化字符串的處理。
在實現利用javascript shell命令進行查詢時,差點繞了一個大圈子:一開始覺得需要做一個javascript shell命令解析器,再配合MongoDB.Driver.Builders.Query類來生成查詢用的QueryDocument。一開始做了一個or命令的解析,例如:{$or:[{Word:'汶川縣'},{Pos:{$gt:256}}]},覺得太難了,各種嵌套怎么解析得完?官方驅動怎么可能對javascript shell命令的支持這么差?於是開始翻源代碼,突然看到一個方法:BsonDocument.Parse!!然后一切都容易了。查詢實現的方法如下:

public List<BsonDocument> GetData(string jsonfind, string jsonsort, int skip, int limit) { var mongo = new MongoClient(string.Format(MongoConst.ConnString, Server.Name)); var server = mongo.GetServer(); var db = server.GetDatabase(Database.Name); var findDoc = string.IsNullOrEmpty(jsonfind) ? new QueryDocument() : new QueryDocument(BsonDocument.Parse(jsonfind)); var sortDoc = string.IsNullOrEmpty(jsonsort) ? new SortByDocument() : new SortByDocument(BsonDocument.Parse(jsonsort)); var query = db.GetCollection(Table.Name).Find(findDoc); query.SetSortOrder(sortDoc); if (skip > 0) { query.Skip = skip; } if (limit > 0) { query.Limit = limit; } return query.ToList(); }
新版系統增加了查看執行計划的功能。在此可對查詢語句的執行效率進行分析,從而協助進行系統優化。目前僅支持【find、sort】的參數設定。
在數據表統計信息的頁面點擊【查看索引】鏈接,或者是點擊樹菜單中的【索引】節點,可進入索引管理頁面。該頁面提供索引列表展示,刪除索引以及添加索引的功能。注意:【索引】的子節點是索引節點,點此節點不做任何操作。
新增索引時,可以使用多個字段生成組合索引。目前不支持當某個字段為文檔類型時,使用內部字段設置索引的情況。另外索引在創建時的一些選項的意義也做了相應的說明。
對於這個新版系統上面介紹了很多,目前還缺少一部分的功能,例如數據的新增和修改的操作;Mapreduce的支持;甚至自定義javascript shell命令的執行(類似執行自定義的sql語句)等……欠缺的功能以后有時間再慢慢補上來的。
在介紹的過程中也穿插的展示了一部分代碼,但沒有對系統的代碼進行完整的介紹,理由是整套代碼幾乎沒有難度,況且代碼就是最好的文檔,而整個系統的代碼量並不大。開發過程主要是加深對MongoDB的一些概念的認識,再配合官方驅動的各種方法進行佐證。
那么,最后,問題來了……代碼在哪里呢?中國XX找XX嗎?當然不是,代碼下載請點下圖: