| 
 | 
 
 
特别是像主题表(topic),用户表(user)等,因为对于一个流量和发帖量都很大的论坛而言,在运行几年之后,这两个表的数据量可能会破千万(注:因为帖子表采用分表机制,所以这里暂未涉及,但出于性能考虑,也提供了本文中类似的解决方案)。当时考虑的架构设计中有两种思路来解决这种问题: 
      一种是采用类似MYSPACE的方式,即按一定记录KEY值(比如用户表的UID)来对大数据表中的记录进行分割,比如前200万用户(即:UID     综上所述,最终我把方向放到了TokyoTyrant,Mongodb上,之所以选择了这两个工具,主要基于下面因素: 
     
    1.海量数据的解决方案应该可以跑在LINUX和WINDOW平台上。当然有人会说Mongodb完全可以跑这两个平台,那还为什么要引入TokyoTyrant呢?其实这里有一些产品的特殊情况要考虑,比如我们的用户中绝大多数对于数据的读写比在 4:1,即5条SQL访问中有4条是SELECT操作,1条是CUD操作,这就造成了读写比例的失衡。虽然Mongodb在读写性能上非常优异和稳定,但在并发读上相对于TokyoTyrant+cabinet还是有一些差距(注:更多内容参见 
[color=]该链接 
,然后这只限于在我们产品中压力测试环境下的结果,不具备普遍性,所以希望大家具体问题具体分析) 
    2.考虑到有些用户公司是有相应技术储备的,两种方案也便于用户公司进行的技术选型(当然因为采用接口方式,用户完全可以引入其它第三方的NOSQL工具来实现)。 
    好了,说了这么多,开始今天的正文吧。 
     
    前面说过,该方案使用了接口方式,这里就先看一下相应的接口声明: 
     
    
  
     
     
     可以看到,目前在企业版中,对主题表(dnt_topics),用户表(dnt_users),在线表(dnt_online)以及帖子表(dnt_posts)进行了NOSQL数据支持,所以定义了如下的几个接口(图中): 
复制代码代码如下:  
public interface ICacheTopics  
public interface ICacheUsers  
public interface ICacheOnlineUser  
public interface ICachePosts  
因为目前只是把这类NOSQL工具当作高级的‘缓存’来用,所以接口命名上都带着‘Cache’的字样。  
然后我使用了一个叫做DBCacheService的类,提供获取这几个接口实例的方法,比如ICacheTopics的实例代码如下:  
复制代码代码如下:  
///   
/// 该类用于获取NoSqlDb声明的缓存服务  
///   
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;  
}  
}  
从上面代码可以看出,使用反射方式获取相应DLL文件(分别是Discuz.EntLib.TokyoTyrant.dll和Discuz.EntLib.MongoDB.dll)中的 类信息并初始化该实例。当然,这里还定义了一个配置文件,也就是EntLibConfigs.GetConfig()这个方法所获取的配置文件信息, 相应 配置文件内容包括:  
复制代码代码如下:  
///   
/// 提供数据库缓存服务,将在线表主题表这类大表放入缓存之中  
///   
public class DBCache  
{  
///   
/// 是否有效  
///   
public bool Enable = false;  
///   
/// 服务地址  
///   
public string Host = "";  
///   
/// 服务地址  
///   
public int Port = 0;  
///   
/// 链接池名称  
///   
public string PoolName = "dnt";  
///   
/// 初始化链接数  
///   
public int IntConnections = 4;  
///   
/// 最少链接数  
///   
public int MinConnections = 4;  
///   
/// 最大连接数  
///   
public int MaxConnections = 4;  
///   
/// avaiable pool池中线程的最大空闲时间  
///   
public int MaxIdle = 30000;  
///   
/// busy pool中线程的最大忙碌时间  
///   
public int MaxBusy = 50000;  
///   
/// 维护线程休息时间  
///   
public int MaintenanceSleep = 300000;  
///   
/// TcpClient读操作超时时间  
///   
public int TcpClientTimeout = 3000;  
///   
/// TcpClient链接超时时间  
///   
public int TcpClientConnectTimeout = 30000;  
///   
/// 缓存类型1为mongodb,2为tokyotyrnat  
///   
public int CacheType = 1;  
}  
上面是配置文件中‘可复用信息’的基类,下面是具体的配置类实例声明:  
复制代码代码如下:  
///   
/// 企业版配置信息类文件  
///   
public class EntLibConfigInfo : IConfigInfo  
{  
///   
/// 提供数据库缓存服务,将在线表(dnt_online)放入CACHE中  
///   
public DBCache Cacheonlineuser = new DBCache();  
///   
/// 提供数据库缓存服务,将用户表(dnt_users)放入CACHE中  
///   
public DBCache Cacheusers = new DBCache();  
///   
/// 提供数据库缓存服务,将主题表(dnt_topic)放入CACHE中  
///   
public DBCache Cachetopics = new DBCache();  
///   
/// 提供数据库缓存服务,将主题表(dnt_topic)放入CACHE中  
///   
public DBCache Cacheposts = new DBCache();  
}  
通过该类,就可以用如下配置文件内容初始化相应的实例了:  
复制代码代码如下:  
  
  
  
10.0.4.119  
27017  
false  
dnt_online  
[I]4[/I]  
4  
4  
30000  
50000  
300000  
3000  
30000  
1  
  
  
  
10.0.4.66  
112121  
false  
dnt_users  
[I]4[/I]  
4  
4  
30000  
50000  
300000  
3000  
30000  
1  
  
  
  
10.0.4.5  
27017  
false  
dnt_topics  
[I]25[/I]  
25  
25  
30000  
5000  
300000  
300000  
30000  
1  
  
  
  
10.0.4.5  
27017  
false  
dnt_posts  
[I]25[/I]  
25  
25  
30000  
5000  
300000  
300000  
30000  
1  
  
  
当然,因为使用的开源的客户源工具在配置上有一定的的差异性(比如命名上等),所以这里有些参数可以对TTCACHE有效,却对MONGODB无效, 不过这并不影响对这两种工具的使用。 
  
      这里要说明的是,对于TokyoTrant而言,这里使用的是我开发的这款客户端软件: 
       
[color=]http://www.cnblogs.com/daizhj/archive/2010/06/08/tokyotyrantclient.html 
 
      Mongodb使用的是: 
[color=]http://github.com/samus/mongodb-csharp 
。 
      
      这里还有个小插曲,之前园子里有朋友介绍了这个客户端 
[color=]NoRM  
,不过在我写了一个LINQ示例并进行压力测试后,发现速度不快,比samus的那个客户端慢了不少,在苦找原因无果的情况下,最终选择了samus,不过在samus中目前也支持LINQ的写法(也算是扩展和尝试吧),如下面的写法(更多具体示例还是参见其官方源码包中的相应内容):  
复制代码代码如下:  
Mongo db = new Mongo("Servers=10.0.4.5:27017;ConnectTimeout=30000;ConnectionLifetime=300000;MinimumPoolSize=64;MaximumPoolSize=256 ooled=true");  
db.Connect();  
var topicColl = db.GetDatabase("dnt_mongodb").GetCollection("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 topicList = new List();  
foreach (var topic in topicInfoList)  
{  
topicList.Add(LoadTopicInfo(topic));  
}  
db.Disconnect();  
return topicList;  
不过在使用上述代码进行1500万主题分页时,发现LR的测试周期延长(前者(document方式)从2:10秒延长到后者(linq)2:30秒)和吞吐量降低。  
所以这里还是最终延用了samus的document访问方式,参照上面的LINQ写法,下面是document写法,形如:  
复制代码代码如下:  
public Discuz.Common.Generic.List GetTopicList(int fid, int pageSize, int pageIndex, int startNumber)  
{  
int skip = 0;  
if (pageIndex  topicInfoList = new Common.Generic.List();  
System.Collections.Generic.List docList = MongoDbHelper.Find(mongoDB, "topics",  
new Document().Add("fid", fid).Add("displayorder", 0), "lastpostid", IndexOrder.Descending, pageSize, skip);  
return docList;  
}  
如果在你的项目中非要使用LINQ方式的话,那在这里再要介绍的一个samus的属性绑定功能,这个功能对于那些数据库字段与代码中的属性存在 “大小写”差异的情况下,非常有用,即对相应实体类进行‘别名’的绑定,比如对于主题表(需引入MongoDB.Attributes名空间):  
复制代码代码如下:  
///   
/// 主题信息描述类  
///   
public class TopicInfo : Discuz.Entity.TopicInfo  
{  
[MongoAlias("attention")]  
public new int Attention { get; set; }  
///  
///主题tid  
///  
[MongoAlias("tid")]  
public new int Tid { get; set; }  
///   
/// 板块名称  
///   
[MongoAlias("forumname")]  
public new string Forumname { get; set; }  
///  
///版块fid  
///  
[MongoAlias("fid")]  
public new int Fid { get; set; }  
///  
///主题图标id  
///  
[MongoAlias("iconid")]  
public new int Iconid { get; set; }  
......  
上面的MongoAlias属性就是属性别名,它就是MONGODB中所存储的数据字段名称。  
介绍到这里,再回到正文。  
因为这两个工具都是在数据库层面进行缓存的,所以它对于原有的DISCUZ!NT中的缓存系统而言,与数据库帖的更近,所以对原有的业务逻辑改造,  
就停留在了数据访问层"DISCUZ.DATA.dll"中了,其实到这里,就看出了当初为什么要分层,以及分层带来的好处了。  
比如在Discuz.Data.Topics这个类中添加了这两个静态变量:  
复制代码代码如下:  
///   
/// 是否启用TokyoTyrantCache缓存用户表  
///   
public static bool appDBCache = (EntLibConfigs.GetConfig() != null && EntLibConfigs.GetConfig().Cachetopics.Enable);  
public static ICacheTopics ITopicService = appDBCache ? DBCacheService.GetTopicsService() : null;  
前者用户判断是否启用主题缓存,后者则获取相应的缓存服务实例(前面配置文件中已做相应说明)。  
这样,在已有的数据访问代码中加入相应的缓存逻辑,比如获取主题信息:  
复制代码代码如下:  
///   
/// 获得主题信息  
///   
/// 要获得的主题ID  
/// 版块ID  
/// 模式选择, 0=当前主题, 1=上一主题, 2=下一主题  
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;  
}  
当然,因为使用了缓存方式,所以就牵扯到缓存中的数据与数据库中数据的一致性问题,所以对于主题的CUD操作,也要对应有相应的对缓存的操作,这基本上就是一个工作量的问题了。因为无论是TTCACHED,还是MONGODB,都支持更新操作。  
比如同样是更新主题附件类型的操作,下面是TTCACHED的写法:  
复制代码代码如下:  
///   
/// 更新主题附件类型  
///   
/// 主题Id  
/// 附件类型,1普通附件,2为图片附件  
///   
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的写法  
复制代码代码如下:  
///   
/// 更新主题附件类型  
///   
/// 主题Id  
/// 附件类型,1普通附件,2为图片附件  
///   
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更新性能要高许多(之后的测试结果也说明了这一点)。 
    
      正如之前所说的那样,如用户对于这两个接口实现方案均不满意,那么他可以使用其它类型的NOSQL数据库,只要实现了相应的接口: 
     public interface ICacheTopics 
     public interface ICacheUsers 
     public interface ICacheOnlineUser 
     public interface ICachePosts      
       并在配置文件中进行相应的配置就可以了,当然本文中代码因为时间问题还是有待考量的,但主要的架构设计思想基本被确定下来了。 
  
  
      当然对于原有的数据库中的记录,如果要使用本方案,我提供了转换工具,用于把数据转到TTCACHED或MONGODB中的任一服务端上。如下: 
  
     TTCACEHD: 
      
  
      
     MongoDB(目前比TTACEHD多了帖子分表转换功能): 
     
  
  
  
      最后在压力测试过程中,还出现了一些小问题,好在对着官方文档,逐步优化解决了,这里要特别说一下MONGDOB,其文件的详细程度要好于TTCACHED,基本上主要的功能都有详细的介绍说明页面,呵呵。当然TTCACHED的诞生时间要比MONGODB早,所以在生产环境下的成功案例也相对多一些。 
      
      
     下面列了一下使用过程中的小问题,仅作记录:             
       
      TokyoTyrant的使用问题:尽量不要在查询的列表中使用排序操作,因为它的排序效率还不如数据库高。尽量使用索引进行查询 
                   键值操作。2000w记录以下查询效率很高,但更高的数据量上目前没做过压力测试(包括CRUD操作) 
       
      Mongodb:尽量使用_ID做为查询键值操作,包括排序等,对索引进行优化(单列或多列进行索引)。 
原文链接:[/url][url=http://www.cnblogs.com/daizhj/archive/2010/07/20/1781140.html] 
[color=]http://www.cnblogs.com/daizhj/archive/2010/07/20/1781140.html 
  
 |   
 
 
 
 |