Discuz!NT千萬級數(shù)據(jù)量上的兩駕馬車 TokyoCabinet,MongoDB

一種是采用類似MYSPACE的方式,即按一定記錄KEY值(比如用戶表的UID)來對大數(shù)據(jù)表中的記錄進行分割,比如前200萬用戶(即:UID<200w)放入一個表,200-400萬的用戶放入另一個表,以此類推。當(dāng)然可以把幾個表都放到一個數(shù)據(jù)庫中,也可以放到別的MSSQL數(shù)據(jù)庫上或?qū)嵗?。但這種方案有一些問題,例如當(dāng)用戶表需要被聯(lián)表(如LEFT JION)查詢時使用,比如我們的帖子表進行分頁查詢時就需要左聯(lián)user表,這時如采用分表或分布式布署就可能面臨這樣的問題,不僅業(yè)務(wù)邏輯要變化,就連存儲過程中也要產(chǎn)生不小的變化,這里還不考慮效率上的問題。當(dāng)然有人建議可以使用數(shù)據(jù)冗余的方式,比如在帖子表中冗余用戶信息相應(yīng)字段,但這種方案同樣要大幅度的修改即有代碼,同時如果用戶信息發(fā)生變化時,不僅要更新用戶表,還要更新帖子表中的相應(yīng)冗余字段,如果這兩者不同步,就會造成數(shù)據(jù)顯示異常,當(dāng)然在數(shù)據(jù)庫層面增加存儲成本也是不得不付出的。
第二種就是使用能處理大數(shù)據(jù)量表格的第三方工具,比如本文所說的TokyoTyrant,Mongodb等,這類NOSQL軟件從一問世就是面向海量數(shù)據(jù)存儲訪問的,而且這類軟件往往都是開源的,另外通過與打算布署企業(yè)版的用戶接觸,發(fā)現(xiàn)雖然他們的服務(wù)器配置很高,但數(shù)量即不多,所以就要考慮如何最大限度的復(fù)用已有的機器資源,而這類NOSQL軟件往往都是‘性價比’很高的,即用不多的資源(內(nèi)存,CPU等)就能達(dá)到意想不到的效果。當(dāng)然我目前對其還是很謹(jǐn)慎的使用,即不會馬上把它當(dāng)做主力數(shù)據(jù)存儲工具,而是輔助MSSQL數(shù)據(jù)庫工具,所以大家在看完本文后會發(fā)現(xiàn),這兩個工具在企業(yè)版中的角色頂多就是一個高級的MEMCACEHD。不過我的想法很簡單,就是任何工具和技術(shù),如果不是很了解它或者它很新,那么必定要有一個“考核期”,如果在‘任間’內(nèi)它通過考核,才委以重任,如未通過考核,也不會讓系統(tǒng)平臺承擔(dān)過多的技術(shù)層面上的‘風(fēng)險’。
綜上所述,最終我把方向放到了TokyoTyrant,Mongodb上,之所以選擇了這兩個工具,主要基于下面因素:
1.海量數(shù)據(jù)的解決方案應(yīng)該可以跑在LINUX和WINDOW平臺上。當(dāng)然有人會說Mongodb完全可以跑這兩個平臺,那還為什么要引入TokyoTyrant呢?其實這里有一些產(chǎn)品的特殊情況要考慮,比如我們的用戶中絕大多數(shù)對于數(shù)據(jù)的讀寫比在 4:1,即5條SQL訪問中有4條是SELECT操作,1條是CUD操作,這就造成了讀寫比例的失衡。雖然Mongodb在讀寫性能上非常優(yōu)異和穩(wěn)定,但在并發(fā)讀上相對于TokyoTyrant+cabinet還是有一些差距(注:更多內(nèi)容參見該鏈接,然后這只限于在我們產(chǎn)品中壓力測試環(huán)境下的結(jié)果,不具備普遍性,所以希望大家具體問題具體分析)
2.考慮到有些用戶公司是有相應(yīng)技術(shù)儲備的,兩種方案也便于用戶公司進行的技術(shù)選型(當(dāng)然因為采用接口方式,用戶完全可以引入其它第三方的NOSQL工具來實現(xiàn))。
好了,說了這么多,開始今天的正文吧。
前面說過,該方案使用了接口方式,這里就先看一下相應(yīng)的接口聲明:
可以看到,目前在企業(yè)版中,對主題表(dnt_topics),用戶表(dnt_users),在線表(dnt_online)以及帖子表(dnt_posts)進行了NOSQL數(shù)據(jù)支持,所以定義了如下的幾個接口(圖中):
public interface ICacheTopics
public interface ICacheUsers
public interface ICacheOnlineUser
public interface ICachePosts
因為目前只是把這類NOSQL工具當(dāng)作高級的‘緩存’來用,所以接口命名上都帶著‘Cache’的字樣。
然后我使用了一個叫做DBCacheService的類,提供獲取這幾個接口實例的方法,比如ICacheTopics的實例代碼如下:
/// <summary>
/// 該類用于獲取NoSqlDb聲明的緩存服務(wù)
/// </summary>
public class DBCacheService
{
static ICacheTopics iCacheTopics = null;
public static ICacheTopics GetTopicsService()
{
if (iCacheTopics == null)
{
lock (lockHelper)
{
if (iCacheTopics == null)
{
try
{
if (EntLibConfigs.GetConfig().Cachetopics.Enable)
{
iCacheTopics = (ICacheTopics)Activator.CreateInstance(Type.GetType(
EntLibConfigs.GetConfig().Cachetopics.CacheType == 2 ?
"Discuz.EntLib.TokyoTyrant.Data.Topics, Discuz.EntLib.TokyoTyrant" :
"Discuz.EntLib.MongoDB.Data.Topics, Discuz.EntLib.MongoDB", false, true));
}
}
catch
{
throw new Exception("請檢查" + (EntLibConfigs.GetConfig().Cachetopics.CacheType == 2 ?
"Discuz.EntLib.TokyoTyrant.dll" :
"Discuz.EntLib.MongoDB.dll") + "文件是否被放置到了bin目錄下!");
}
}
}
}
return iCacheTopics;
}
}
從上面代碼可以看出,使用反射方式獲取相應(yīng)DLL文件(分別是Discuz.EntLib.TokyoTyrant.dll和Discuz.EntLib.MongoDB.dll)中的 類信息并初始化該實例。當(dāng)然,這里還定義了一個配置文件,也就是EntLibConfigs.GetConfig()這個方法所獲取的配置文件信息, 相應(yīng) 配置文件內(nèi)容包括:
/// <summary>
/// 提供數(shù)據(jù)庫緩存服務(wù),將在線表主題表這類大表放入緩存之中
/// </summary>
public class DBCache
{
/// <summary>
/// 是否有效
/// </summary>
public bool Enable = false;
/// <summary>
/// 服務(wù)地址
/// </summary>
public string Host = "";
/// <summary>
/// 服務(wù)地址
/// </summary>
public int Port = 0;
/// <summary>
/// 鏈接池名稱
/// </summary>
public string PoolName = "dnt";
/// <summary>
/// 初始化鏈接數(shù)
/// </summary>
public int IntConnections = 4;
/// <summary>
/// 最少鏈接數(shù)
/// </summary>
public int MinConnections = 4;
/// <summary>
/// 最大連接數(shù)
/// </summary>
public int MaxConnections = 4;
/// <summary>
/// avaiable pool池中線程的最大空閑時間
/// </summary>
public int MaxIdle = 30000;
/// <summary>
/// busy pool中線程的最大忙碌時間
/// </summary>
public int MaxBusy = 50000;
/// <summary>
/// 維護線程休息時間
/// </summary>
public int MaintenanceSleep = 300000;
/// <summary>
/// TcpClient讀操作超時時間
/// </summary>
public int TcpClientTimeout = 3000;
/// <summary>
/// TcpClient鏈接超時時間
/// </summary>
public int TcpClientConnectTimeout = 30000;
/// <summary>
/// 緩存類型1為mongodb,2為tokyotyrnat
/// </summary>
public int CacheType = 1;
}
上面是配置文件中‘可復(fù)用信息’的基類,下面是具體的配置類實例聲明:
/// <summary>
/// 企業(yè)版配置信息類文件
/// </summary>
public class EntLibConfigInfo : IConfigInfo
{
/// <summary>
/// 提供數(shù)據(jù)庫緩存服務(wù),將在線表(dnt_online)放入CACHE中
/// </summary>
public DBCache Cacheonlineuser = new DBCache();
/// <summary>
/// 提供數(shù)據(jù)庫緩存服務(wù),將用戶表(dnt_users)放入CACHE中
/// </summary>
public DBCache Cacheusers = new DBCache();
/// <summary>
/// 提供數(shù)據(jù)庫緩存服務(wù),將主題表(dnt_topic)放入CACHE中
/// </summary>
public DBCache Cachetopics = new DBCache();
/// <summary>
/// 提供數(shù)據(jù)庫緩存服務(wù),將主題表(dnt_topic)放入CACHE中
/// </summary>
public DBCache Cacheposts = new DBCache();
}
通過該類,就可以用如下配置文件內(nèi)容初始化相應(yīng)的實例了:
<EntLibConfigInfo>
<Cacheonlineuser>
<!--在開啟該功能之前,請確保相關(guān)服務(wù)已配置完畢-->
<Host>10.0.4.119</Host>
<Port>27017</Port>
<Enable>false</Enable>
<PoolName>dnt_online</PoolName>
<IntConnections>4</IntConnections>
<MinConnections>4</MinConnections>
<MaxConnections>4</MaxConnections>
<MaxIdle>30000</MaxIdle>
<MaxBusy>50000</MaxBusy>
<MaintenanceSleep>300000</MaintenanceSleep>
<TcpClientTimeout>3000</TcpClientTimeout>
<TcpClientConnectTimeout>30000</TcpClientConnectTimeout>
<CacheType>1</CacheType>
</Cacheonlineuser>
<Cacheusers>
<!--在開啟該功能之前,請確保相關(guān)服務(wù)已配置完畢-->
<Host>10.0.4.66</Host>
<Port>112121</Port>
<Enable>false</Enable>
<PoolName>dnt_users</PoolName>
<IntConnections>4</IntConnections>
<MinConnections>4</MinConnections>
<MaxConnections>4</MaxConnections>
<MaxIdle>30000</MaxIdle>
<MaxBusy>50000</MaxBusy>
<MaintenanceSleep>300000</MaintenanceSleep>
<TcpClientTimeout>3000</TcpClientTimeout>
<TcpClientConnectTimeout>30000</TcpClientConnectTimeout>
<CacheType>1</CacheType>
</Cacheusers>
<Cachetopics>
<!--在開啟該功能之前,請確保相關(guān)服務(wù)已配置完畢-->
<Host>10.0.4.5</Host>
<Port>27017</Port>
<Enable>false</Enable>
<PoolName>dnt_topics</PoolName>
<IntConnections>25</IntConnections>
<MinConnections>25</MinConnections>
<MaxConnections>25</MaxConnections>
<MaxIdle>30000</MaxIdle>
<MaxBusy>5000</MaxBusy>
<MaintenanceSleep>300000</MaintenanceSleep>
<TcpClientTimeout>300000</TcpClientTimeout>
<TcpClientConnectTimeout>30000</TcpClientConnectTimeout>
<CacheType>1</CacheType>
</Cachetopics>
<Cacheposts>
<!--在開啟該功能之前,請確保相關(guān)服務(wù)已配置完畢-->
<Host>10.0.4.5</Host>
<Port>27017</Port>
<Enable>false</Enable>
<PoolName>dnt_posts</PoolName>
<IntConnections>25</IntConnections>
<MinConnections>25</MinConnections>
<MaxConnections>25</MaxConnections>
<MaxIdle>30000</MaxIdle>
<MaxBusy>5000</MaxBusy>
<MaintenanceSleep>300000</MaintenanceSleep>
<TcpClientTimeout>300000</TcpClientTimeout>
<TcpClientConnectTimeout>30000</TcpClientConnectTimeout>
<CacheType>1</CacheType>
</Cacheposts>
</EntLibConfigInfo>
當(dāng)然,因為使用的開源的客戶源工具在配置上有一定的的差異性(比如命名上等),所以這里有些參數(shù)可以對TTCACHE有效,卻對MONGODB無效, 不過這并不影響對這兩種工具的使用。
這里要說明的是,對于TokyoTrant而言,這里使用的是我開發(fā)的這款客戶端軟件:
http://www.cnblogs.com/daizhj/archive/2010/06/08/tokyotyrantclient.html
Mongodb使用的是:http://github.com/samus/mongodb-csharp。
這里還有個小插曲,之前園子里有朋友介紹了這個客戶端NoRM ,不過在我寫了一個LINQ示例并進行壓力測試后,發(fā)現(xiàn)速度不快,比samus的那個客戶端慢了不少,在苦找原因無果的情況下,最終選擇了samus,不過在samus中目前也支持LINQ的寫法(也算是擴展和嘗試吧),如下面的寫法(更多具體示例還是參見其官方源碼包中的相應(yīng)內(nèi)容):
Mongo db = new Mongo("Servers=10.0.4.5:27017;ConnectTimeout=30000;ConnectionLifetime=300000;MinimumPoolSize=64;MaximumPoolSize=256;Pooled=true");
db.Connect();
var topicColl = db.GetDatabase("dnt_mongodb").GetCollection<Discuz.EntLib.MongoDB.Entity.TopicInfo>("topics");
var topicInfoList = topicColl.Linq().Where(t => t.Fid == 2 && t.Displayorder == 0).Skip(skip).OrderByDescending(t=>t.Lastpostid).Take(16).ToList();
Discuz.Common.Generic.List<TopicInfo> topicList = new List<TopicInfo>();
foreach (var topic in topicInfoList)
{
topicList.Add(LoadTopicInfo(topic));
}
db.Disconnect();
return topicList;
不過在使用上述代碼進行1500萬主題分頁時,發(fā)現(xiàn)LR的測試周期延長(前者(document方式)從2:10秒延長到后者(linq)2:30秒)和吞吐量降低。
所以這里還是最終延用了samus的document訪問方式,參照上面的LINQ寫法,下面是document寫法,形如:
public Discuz.Common.Generic.List<TopicInfo> GetTopicList(int fid, int pageSize, int pageIndex, int startNumber)
{
int skip = 0;
if (pageIndex <= 1)
pageSize = pageSize - startNumber;
else
skip = (pageIndex - 1) * pageSize - startNumber;
Discuz.Common.Generic.List<TopicInfo> topicInfoList = new Common.Generic.List<TopicInfo>();
System.Collections.Generic.List<Document> docList = MongoDbHelper.Find(mongoDB, "topics",
new Document().Add("fid", fid).Add("displayorder", 0), "lastpostid", IndexOrder.Descending, pageSize, skip);
return docList;
}
如果在你的項目中非要使用LINQ方式的話,那在這里再要介紹的一個samus的屬性綁定功能,這個功能對于那些數(shù)據(jù)庫字段與代碼中的屬性存在 “大小寫”差異的情況下,非常有用,即對相應(yīng)實體類進行‘別名’的綁定,比如對于主題表(需引入MongoDB.Attributes名空間):
/// <summary>
/// 主題信息描述類
/// </summary>
public class TopicInfo : Discuz.Entity.TopicInfo
{
[MongoAlias("attention")]
public new int Attention { get; set; }
///<summary>
///主題tid
///</summary>
[MongoAlias("tid")]
public new int Tid { get; set; }
/// <summary>
/// 板塊名稱
/// </summary>
[MongoAlias("forumname")]
public new string Forumname { get; set; }
///<summary>
///版塊fid
///</summary>
[MongoAlias("fid")]
public new int Fid { get; set; }
///<summary>
///主題圖標(biāo)id
///</summary>
[MongoAlias("iconid")]
public new int Iconid { get; set; }
......
上面的MongoAlias屬性就是屬性別名,它就是MONGODB中所存儲的數(shù)據(jù)字段名稱。
介紹到這里,再回到正文。
因為這兩個工具都是在數(shù)據(jù)庫層面進行緩存的,所以它對于原有的DISCUZ!NT中的緩存系統(tǒng)而言,與數(shù)據(jù)庫帖的更近,所以對原有的業(yè)務(wù)邏輯改造,
就停留在了數(shù)據(jù)訪問層"DISCUZ.DATA.dll"中了,其實到這里,就看出了當(dāng)初為什么要分層,以及分層帶來的好處了。
比如在Discuz.Data.Topics這個類中添加了這兩個靜態(tài)變量:
/// <summary>
/// 是否啟用TokyoTyrantCache緩存用戶表
/// </summary>
public static bool appDBCache = (EntLibConfigs.GetConfig() != null && EntLibConfigs.GetConfig().Cachetopics.Enable);
public static ICacheTopics ITopicService = appDBCache ? DBCacheService.GetTopicsService() : null;
前者用戶判斷是否啟用主題緩存,后者則獲取相應(yīng)的緩存服務(wù)實例(前面配置文件中已做相應(yīng)說明)。
這樣,在已有的數(shù)據(jù)訪問代碼中加入相應(yīng)的緩存邏輯,比如獲取主題信息:
/// <summary>
/// 獲得主題信息
/// </summary>
/// <param name="tid">要獲得的主題ID</param>
/// <param name="fid">版塊ID</param>
/// <param name="mode">模式選擇, 0=當(dāng)前主題, 1=上一主題, 2=下一主題</param>
public static TopicInfo GetTopicInfo(int tid, int fid, byte mode)
{
TopicInfo topicInfo = null;
if (appDBCache)//新增代碼
topicInfo = ITopicService.GetTopicInfo(tid, fid, mode);
if(topicInfo == null)
{
//原代碼
IDataReader reader = DatabaseProvider.GetInstance().GetTopicInfo(tid, fid, mode);
if (reader.Read())
topicInfo = LoadSingleTopicInfo(reader);
reader.Close();
if (appDBCache && topicInfo != null)
ITopicService.CreateTopic(topicInfo);
}
return topicInfo;
}
當(dāng)然,因為使用了緩存方式,所以就牽扯到緩存中的數(shù)據(jù)與數(shù)據(jù)庫中數(shù)據(jù)的一致性問題,所以對于主題的CUD操作,也要對應(yīng)有相應(yīng)的對緩存的操作,這基本上就是一個工作量的問題了。因為無論是TTCACHED,還是MONGODB,都支持更新操作。
比如同樣是更新主題附件類型的操作,下面是TTCACHED的寫法:
/// <summary>
/// 更新主題附件類型
/// </summary>
/// <param name="tid">主題Id</param>
/// <param name="attType">附件類型,1普通附件,2為圖片附件</param>
/// <returns></returns>
public int UpdateTopicAttachmentType(int tid, int attType)
{
var qrecords = TokyoTyrantService.QueryRecords(pool, new Query().NumberEquals("tid", tid));
foreach (string key in qrecords.Keys)
{
var column = qrecords[key];
column["attachment"] = attType.ToString();
TokyoTyrantService.PutColumns(pool, column["tid"], column, true);
break;
}
return 1;
}
下面是MongoDB的寫法
/// <summary>
/// 更新主題附件類型
/// </summary>
/// <param name="tid">主題Id</param>
/// <param name="attType">附件類型,1普通附件,2為圖片附件</param>
/// <returns></returns>
public int UpdateTopicAttachmentType(int tid, int attType)
{
MongoDbHelper.Update(mongoDB, "topics",
new Document() { { "$set", new Document() { { "attachment", attType } } } },
new Document().Add("_id", tid));
return 1;
}
通過對比可以看出,MONGODB可以對某一字段進行操作,而TTCACEHD則只能通過查詢先獲取整條記錄,然后修改某一‘字段’,之后再整條提交更新,所以單從這一角度講,MONGDOB要比TTCACHED更新性能要高許多(之后的測試結(jié)果也說明了這一點)。
正如之前所說的那樣,如用戶對于這兩個接口實現(xiàn)方案均不滿意,那么他可以使用其它類型的NOSQL數(shù)據(jù)庫,只要實現(xiàn)了相應(yīng)的接口:
public interface ICacheTopics
public interface ICacheUsers
public interface ICacheOnlineUser
public interface ICachePosts
并在配置文件中進行相應(yīng)的配置就可以了,當(dāng)然本文中代碼因為時間問題還是有待考量的,但主要的架構(gòu)設(shè)計思想基本被確定下來了。
當(dāng)然對于原有的數(shù)據(jù)庫中的記錄,如果要使用本方案,我提供了轉(zhuǎn)換工具,用于把數(shù)據(jù)轉(zhuǎn)到TTCACHED或MONGODB中的任一服務(wù)端上。如下:
TTCACEHD:

MongoDB(目前比TTACEHD多了帖子分表轉(zhuǎn)換功能):

最后在壓力測試過程中,還出現(xiàn)了一些小問題,好在對著官方文檔,逐步優(yōu)化解決了,這里要特別說一下MONGDOB,其文件的詳細(xì)程度要好于TTCACHED,基本上主要的功能都有詳細(xì)的介紹說明頁面,呵呵。當(dāng)然TTCACHED的誕生時間要比MONGODB早,所以在生產(chǎn)環(huán)境下的成功案例也相對多一些。
下面列了一下使用過程中的小問題,僅作記錄:
TokyoTyrant的使用問題:盡量不要在查詢的列表中使用排序操作,因為它的排序效率還不如數(shù)據(jù)庫高。盡量使用索引進行查詢
鍵值操作。2000w記錄以下查詢效率很高,但更高的數(shù)據(jù)量上目前沒做過壓力測試(包括CRUD操作)
Mongodb:盡量使用_ID做為查詢鍵值操作,包括排序等,對索引進行優(yōu)化(單列或多列進行索引)。
原文鏈接:http://www.cnblogs.com/daizhj/archive/2010/07/20/1781140.html
相關(guān)文章
O'Reilly:深入學(xué)習(xí)MongoDB(中文版) PDF 掃描版[8M]
分別對應(yīng)O'Reilly公司出版的Scaling MongoDB和50 Tips and Tricks for MongoDB Developers兩本書的內(nèi)容2013-07-05MongoDB權(quán)威指南 (美) 霍多羅夫著 中文 PDF版
MongoDB如何幫你管理通過Web應(yīng)用收集的海量數(shù)據(jù)呢?通過本書的權(quán)威解讀,你會了解面向文檔數(shù)據(jù)庫的諸多優(yōu)點,會發(fā)現(xiàn)MongoDB如此穩(wěn)定、性能優(yōu)越甚至能夠無限水平擴展背后的2012-12-19MongoDB For Linux 數(shù)據(jù)庫服務(wù)器 v2.4.5 官方版
MongoDB是一個介于關(guān)系數(shù)據(jù)庫和非關(guān)系數(shù)據(jù)庫之間的產(chǎn)品,是非關(guān)系數(shù)據(jù)庫當(dāng)中功能最豐富,最像關(guān)系數(shù)據(jù)庫的2013-08-04MongoDB管理與開發(fā)精要 紅丸出品 中文 pdf版
MongoDB管理與開發(fā)精要》的同名電子書2012-05-08RockMongo php MongoDB管理工具 v1.1.5
RockMongo是一個PHP5寫的MongoDB管理工具。2013-02-09RockMongo MongoDB數(shù)據(jù)庫管理工具(php) v1.0.5
RockMongo 是一個PHP5寫的MongoDB管理工具。 主要特征: 使用寬松的New BSD License協(xié)議 速度快,安裝簡單 系統(tǒng) 可以配置多個主機,每個主機可以有多個管理員 需要2010-08-24mongoDB For Windows 數(shù)據(jù)庫服務(wù)器 V2.4.5 官方正式版
MongoDB是一個介于關(guān)系數(shù)據(jù)庫和非關(guān)系數(shù)據(jù)庫之間的產(chǎn)品,是非關(guān)系數(shù)據(jù)庫當(dāng)中、功能最豐富,最像關(guān)系數(shù)據(jù)庫的.2013-08-04