Discuz!NT中集成Memcached分布式缓存
大约在两年前我写过一篇关于Discuz!NT缓存架构的文章,在那篇文章的结尾介绍了在IIS中如果开启多个 应用程序池会造成多个缓存实例之间数据同步的问题。虽然给出了一个解决方案,但无形中却把压力转移到了 磁盘I/O上(多个进程并发访问cache.config文件)。其实从那时起我就开始关注有什么更好的方案,当然今 天本文中所说的Memcached,以及Velocity等这类的分布式缓存方案之前都考虑过,但一直未能决定该使用那 个。起码Velocity要在.net4.0之后才会提供,虽然是原生态,但有些远水解不了近火。
我想真正等到Velocity能堪当重任还要等上一段时间。于是我就开始将注意力转移到了Memcached,必定 有Facebook这只“超级小白鼠”使用它并且反响还不错。所以就开始尝试动手在产品中集成Memcached。
其实在之前的那篇关于Discuz!NT缓存架构的文章中已提到过,使用了设计模式中的“策略模式”来构造。 所以为了与以往使用缓存的代码格式相兼容,所以这里采用新添加MemCachedStrategy(MemCached策略) 来构造一个缓存策略类以便于当管理后台开启“MemCached”时以“MemCached策略模式”来做为当前系统默认 的策略模式。
其代码段如下(Discuz.Cache/MemCached.cs):
复制代码 /// ///MemCache缓存策略类 /// publicclassMemCachedStrategy:Discuz.Cache.ICacheStrategy {
/// ///添加指定ID的对象 /// /// /// publicvoidAddObject(stringobjId,objecto) { RemoveObject(objId); if(TimeOut>0) { MemCachedManager.CacheClient.Set(objId,o,System.DateTime.Now.AddMinutes(TimeOut)); } else { MemCachedManager.CacheClient.Set(objId,o); } }
/// ///添加指定ID的对象(关联指定文件组) /// /// /// /// publicvoidAddObjectWithFileChange(stringobjId,objecto,string[]files) { ; }
/// ///添加指定ID的对象(关联指定键值组) /// /// /// /// publicvoidAddObjectWithDepend(stringobjId,objecto,string[]dependKey) { ; }
/// ///移除指定ID的对象 /// /// publicvoidRemoveObject(stringobjId) { if(MemCachedManager.CacheClient.KeyExists(objId)) MemCachedManager.CacheClient.Delete(objId); }
/// ///返回指定ID的对象 /// /// /// publicobjectRetrieveObject(stringobjId) { returnMemCachedManager.CacheClient.Get(objId); }
/// ///到期时间 /// publicintTimeOut{set;get;} } 复制代码
上面类实现的接口Discuz.Cache.ICacheStrategy定义如下:
复制代码 /// ///公共缓存策略接口 /// publicinterfaceICacheStrategy { /// ///添加指定ID的对象 /// /// /// voidAddObject(stringobjId,objecto);
/// ///添加指定ID的对象(关联指定文件组) /// /// /// /// voidAddObjectWithFileChange(stringobjId,objecto,string[]files);
/// ///添加指定ID的对象(关联指定键值组) /// /// /// /// voidAddObjectWithDepend(stringobjId,objecto,string[]dependKey);
/// ///移除指定ID的对象 /// /// voidRemoveObject(stringobjId);
/// ///返回指定ID的对象 /// /// /// objectRetrieveObject(stringobjId);
/// ///到期时间 /// intTimeOut{set;get;} } 复制代码
当然在MemCachedStrategy类中还有一个对象要加以说明,就是MemCachedManager,该类主要是对 Memcached一些常操作和相关初始化实例调用的“封装”,下面是是其变量定义和初始化构造方法的代码:
复制代码 /// ///MemCache管理操作类 /// publicsealedclassMemCachedManager { #region静态方法和属性 privatestaticMemcachedClientmc=null;
privatestaticSockIOPoolpool=null;
privatestaticMemCachedConfigInfomemCachedConfigInfo=MemCachedConfigs.GetConfig();
privatestaticstring[]serverList=null;
staticMemCachedManager() { CreateManager(); }
privatestaticvoidCreateManager() { serverList=Utils.SplitString(memCachedConfigInfo.ServerList,""r"n");
pool=SockIOPool.GetInstance(memCachedConfigInfo.PoolName); pool.SetServers(serverList); pool.InitConnections=memCachedConfigInfo.IntConnections;//初始化链接数 pool.MinConnections=memCachedConfigInfo.MinConnections;//最少链接数 pool.MaxConnections=memCachedConfigInfo.MaxConnections;//最大连接数 pool.Socketwww.wang027.comConnectTimeout=memCachedConfigInfo. SocketConnectTimeout;//Socket链接超时时间 pool.SocketTimeout=memCachedConfigInfo.SocketTimeout;//Socket超时时间 pool.MaintenanceSleep=memCachedConfigInfo.MaintenanceSleep;//维护线程休息时间 pool.Failover=memCachedConfigInfo.FailOver;//失效转移(一种备份操作模式) pool.Nagle=memCachedConfigInfo.Nagle;//是否用nagle算法启动socket pool.HashingAlgorithm=HashingAlgorithm.NewCompatibleHash; pool.Initialize();
mc=newMemcachedClient(); mc.PoolName=memCachedConfigInfo.PoolName; mc.EnableCompression=false; }
/// ///缓存服务器地址列表 /// publicstaticstring[]ServerList { set { if(value!=null) serverList=value; } get{returnserverList;} }
/// ///客户端缓存操作对象 /// publicstaticMemcachedClientCacheClient { get { if(mc==null) CreateManager();
returnmc; } }
publicstaticvoidDispose() { if(pool!=null) pool.Shutdown(); }
复制代码
上面代码中构造方法会初始化一个池来管理执行Socket链接,并提供静态属性CacheClient以便MemCachedStrategy 来调用。
当然我还在这个管理操作类中添加了几个方法分别用于检测当前有效的分布式缓存服务器的列表,向指定(或全部) 缓存服务器发送特定stats命令来获取当前缓存服务器上的数据信息和内存分配信息等,相应的方法如下(详情见注释):
复制代码 /// ///获取当前缓存键值所存储在的服务器 /// ///当前缓存键 ///当前缓存键值所存储在的服务器 publicstaticstringGetSocketHost(stringkey) { stringhostName=""; SockIOsock=null; try { sock=SockIOPool.GetInstance(memCachedConfigInfo.PoolName).GetSock(key); if(sock!=null) { hostName=sock.Host; } } finally { if(sock!=null) sock.Close(); } returnhostName; }
/// ///获取有效的服务器地址 /// ///有效的服务器地 publicstaticstring[]GetConnectedSocketHost() { SockIOsock=null; stringconnectedHost=null; foreach(stringhostNameinserverList) { if(!Discuz.Common.Utils.StrIsNullOrEmpty(hostName)) { try { sock=SockIOPool.GetInstance(memCachedConfigInfo.PoolName).GetConnection(hostName); if(sock!=null) { connectedHost=Discuz.Common.Utils.MergeString(hostName,connectedHost); } } finally { if(sock!=null) sock.Close(); } } } returnDiscuz.Common.Utils.SplitString(connectedHost,","); }
/// ///获取服务器端缓存的数据信息 /// ///返回信息 publicstaticArrayListGetStats() { ArrayListarrayList=newArrayList(); foreach(stringserverinserverList) { arrayList.Add(server); } returnGetStats(arrayList,Stats.Default,null); }
/// ///获取服务器端缓存的数据信息 /// ///要访问的服务列表 ///返回信息 publicstaticArrayListGetStats(ArrayListserverArrayList,StatsstatsCommand,stringparam) { ArrayListstatsArray=newArrayList(); param=Utils.StrIsNullOrEmpty(param)?"":param.Trim().ToLower();
stringcommandstr="stats"; //转换stats命令参数 switch(statsCommand) { caseStats.Reset:{commandstr="statsreset";break;} caseStats.Malloc:{commandstr="statsmalloc";break;} caseStats.Maps:{commandstr="statsmaps";break;} caseStats.Sizes:{commandstr="statssizes";break;} caseStats.Slabs:{commandstr="statsslabs";break;} caseStats.Items:{commandstr="stats";break;} caseStats.CachedDump: { string[]statsparams=Utils.SplitString(param,""); if(statsparams.Length==2) if(Utils.IsNumericArray(statsparams)) commandstr="statscachedump"+param;
break; } caseStats.Detail: { if(string.Equals(param,"on")||string.Equals(param,"off")||string.Equals(param,"dump")) commandstr="statsdetail"+param.Trim();
break; } default:{commandstr="stats";break;} } //加载返回值 Hashtablestats=MemCachedManager.CacheClient.Stats(serverArrayList,commandstr); foreach(stringkeyinstats.Keys) { statsArray.Add(key); Hashtablevalues=(Hashtable)stats[key]; foreach(stringkey2invalues.Keys) { statsArray.Add(key2+":"+values[key2]); } } returnstatsArray; }
/// ///Stats命令行参数 /// publicenumStats { /// ///stats:显示服务器信息,统计数据等 /// Default=0, /// ///statsreset:清空统计数据 /// Reset=1, /// ///statsmalloc:显示内存分配数据 /// Malloc=2, /// ///statsmaps:显示"/proc/self/maps"数据 /// Maps=3, /// ///statssizes /// Sizes=4, /// ///statsslabs:显示各个slab的信息,包括chunk的大小,数目,使用情况等 /// Slabs=5, /// ///statsitems:显示各个slab中item的数目和最老item的年龄(最后一次访问距离现在的秒数) /// Items=6, /// ///statscachedumpslab_idlimit_num:显示某个slab中的前limit_num个key列表 /// CachedDump=7, /// ///statsdetail[on|off|dump]:设置或者显示详细操作记录on:打开详细操作记录off:关闭详细操作记录dump:显示详细操作记录(每一个键值get,set,hit,del的次数) /// Detail=8 } 复制代码
当然在配置初始化缓存链接池时使用了配置文件方式(memcached.config)来管理相关参数,其info信息 类说明如下(Discuz.Config/MemCachedConfigInfo.cs):
复制代码 /// ///MemCached配置信息类文件 /// publicclassMemCachedConfigInfo:IConfigInfo { privatebool_applyMemCached; /// ///是否应用MemCached /// publicboolApplyMemCached { get { return_applyMemCached; } set { _applyMemCached=value; } }
privatestring_serverList; /// ///链接地址 /// publicstringServerList { get { return_serverList; } set { _serverList=value; } }
privatestring_poolName; /// ///链接池名称 /// publicstringPoolName { get { returnUtils.StrIsNullOrEmpty(_poolName)?"DiscuzNT_MemCache":_poolName; } set { _poolName=value; } }
privateint_intConnections; /// ///初始化链接数 /// publicintIntConnections { get { return_intConnections>0?_intConnections:3; } set { _intConnections=value; } }
privateint_minConnections; /// ///最少链接数 /// publicintMinConnections { get { return_minConnections>0?_minConnections:3; } set { _minConnections=value; } }
privateint_maxConnections; /// ///最大连接数 /// publicintMaxConnections { get { return_maxConnections>0?_maxConnections:5; } set { _maxConnections=value; } }
privateint_socketConnectTimeout; /// ///Socket链接超时时间 /// publicintSocketConnectTimeout { get { return_socketConnectTimeout>1000?_socketConnectTimeout:1000; } set { _socketConnectTimeout=value; } }
privateint_socketTimeout; /// ///socket超时时间 /// publicintSocketTimeout { get { return_socketTimeout>1000?_maintenanceSleep:3000; } set { _socketTimeout=value; } }
privateint_maintenanceSleep; /// ///维护线程休息时间 /// publicintMaintenanceSleep { get { return_maintenanceSleep>0?_maintenanceSleep:30; } set { _maintenanceSleep=value; } }
privatebool_failOver; /// ///链接失败后是否重启,详情参见http://baike.baidu.com/view/1084309.htm /// publicboolFailOver { get { return_failOver; } set { _failOver=value; } }
privatebool_nagle; /// ///是否用nagle算法启动socket /// publicboolNagle { get { return_nagle; } set { _nagle=value; } } } 复制代码
这些参数我们通过注释应该有一些了解,可以说memcached的主要性能都是通过这些参数来决定的,大家 应根据自己公司产品和应用的实际情况配置相应的数值。
当然,做完这一步之后就是对调用“缓存策略”的主体类进行修改来,使其根据对管理后台的设计来决定 加载什么样的缓存策略,如下:
复制代码 /// ///Discuz!NT缓存类 ///对Discuz!NT论坛缓存进行全局控制管理 /// publicclassDNTCache { .
//通过该变量决定是否启用MemCached privatestaticboolapplyMemCached=MemCachedConfigs.GetConfig().ApplyMemCached;
/// ///构造函数 /// privateDNTCache() { if(applyMemCached) cs=newMemCachedStrategy(); else { cs=newDefaultCacheStrategy();
objectXmlMap=rootXml.CreateElement("Cache"); //建立内部XML文档. rootXml.AppendChild(objectXmlMap);
//LogVisitorclv=newCacheLogVisitor(); //cs.Accept(clv);
cacheConfigTimer.AutoReset=true; cacheConfigTimer.Enabled=true; cacheConfigTimer.Elapsed+=newSystem.Timers.ElapsedEventHandler(Timer_Elapsed); cacheConfigTimer.Start(); } }
复制代码
到这里,主要的开发和修改基本上就告一段落了。下面开始介绍一下如果使用Stats命令来查看缓存的 分配和使用等情况。之前在枚举类型Stats中看到该命令有几个主要的参数,分别是:
复制代码 stats statsreset statsmalloc statsmaps statssizes statsslabs statsitems statscachedumpslab_idlimit_num statsdetail[on|off|dump] 复制代码
而JAVAEYE的robbin写过一篇文章:贴一段遍历memcached缓存对象的小脚本,来介绍如何使用其中的 “statscachedump”来获取信息。受这篇文章的启发,我将MemCachedClient.cs文件中的Stats方法加以修 改,添加了一个command参数(字符串型),这样就可以向缓存服务器发送上面所说的那几种类型的命令了。
测试代码如下:
复制代码 protectedvoidSubmit_Click(objectsender,EventArgse) { ArrayListarrayList=newArrayList(); arrayList.Add("10.0.1.52:11211");//缓存服务器的地址
StateResult.DataSource=MemCachedManager.GetStats(arrayList,(MemCachedManager.Stats) Utils.StrToInt(StatsParam.SelectedValue,0),Param.Text); StateResult.DataBind(); } |
|