在开发LLServer的同时,我一直在跟进测试企业版的相应LLServer客户端,目前这部分代码已测试完毕并提交的Discuz!NT产品中,会跟随最新的源码包一并发布。本文主要是介绍一下产品中引入LLServer的架构思路。
在Discuz!NT的企业版产品中,使用了Memcached,Redis这两个软件来提供分布式缓存服务(两者任选其一)。现有又有了LLServer,它不仅提供了KEY/VALUE缓存,还包括持久化存储部分。这样,用户可以有更多大的选择余地。
下面是Discuz!NT的企业版分布式缓存中一个架构图(DNTCache用于包含调用cacheStrategy):
我们通过配置相应的config文件来决定使用那种类型的缓存服务。当然其也有一个优先顺序,即:memcached, redis, llserver。这可以参照最新版的DNTCache.cs文件(位于Discuz.Cache项目下),部分代码参见如下:
/// <summary> /// 构造函数 /// </summary> private DNTCache(){ if (MemCachedConfigs.GetConfig() != null && MemCachedConfigs.GetConfig().ApplyMemCached) applyMemCached = true ; if (RedisConfigs.GetConfig() != null && RedisConfigs.GetConfig().ApplyRedis) applyRedis = true ; if (LLServerConfigs.GetConfig() != null && LLServerConfigs.GetConfig().ApplyLLServer) applyLLServer = true ; if (applyMemCached || applyRedis || applyLLServer) { try { string cacheStratetyName; if (applyMemCached) cacheStratetyName = " MemCachedStrategy " ; else if (applyRedis) cacheStratetyName = " RedisStrategy " ; else cacheStratetyName = " LLStrategy " ; cs = cachedStrategy = (ICacheStrategy)Activator.CreateInstance(Type.GetType( " Discuz.EntLib. " + cacheStratetyName + " , Discuz.EntLib " , false , true )); } catch { throw new Exception( " 请检查Discuz.EntLib.dll文件是否被放置在bin目录下并配置正确 " ); } } else { cs = new DefaultCacheStrategy(); if (rootXml.HasChildNodes) rootXml.RemoveAll(); objectXmlMap = rootXml.CreateElement( " Cache " ); // 建立内部XML文档. rootXml.AppendChild(objectXmlMap); } } 当memcached.config及redis.config文件的Apply..选项为false时,这时如启用llserver.config文件的如下节点,即可启动llserver。
< ApplyLLServer > true </ ApplyLLServer > 注:有关llserver的安装使用信息请参见如下链接:
下面介绍一下我们企业版中LLSERVER的客户端的设计思路。熟悉我们产品的朋友知道我们的缓存设计基于stratety模式,之前的memcached,redis都有相应的策略实现,详情参见下面两个链接:
这里对LLServer也不例外,同样引入了相应的策略实现,如下:
Discuz.EntLib\LLServer\LLStrategy.cs Discuz.EntLib\LLServer\LLManager.cs
顾名思义,LLStrategy.cs即是策略实现,LLManager.cs只是一个访问LLServer服务端的一个客户端封装。下面分别看一下源代码,首先是LLStrategy.cs:
/// <summary> /// 企业级llserver缓存策略类 /// </summary> public class LLStrategy : DefaultCacheStrategy{ /// <summary> /// 添加指定ID的对象 /// </summary> /// <param name="objId"></param> /// <param name="o"></param> public override void AddObject( string objId, object o) { if ( ! objId.StartsWith( " /Forum/ShowTopic/ " )) base .AddObject(objId, o, LocalCacheTime); LLManager.Set(objId, o); RecordLog(objId, " set " ); } /// <summary> /// 加入当前对象到缓存中 /// </summary> /// <param name="objId"> 对象的键值 </param> /// <param name="o"> 缓存的对象 </param> /// <param name="o"> 到期时间,单位:秒 </param> public override void AddObject( string objId, object o, int expire) { // 凡是以"/Forum/ShowTopic/"为前缀不添加到本地缓存中,现类似键值包括: "/Forum/ShowTopic/Tag/{topicid}/" , "/Forum/ShowTopic/TopList/{fid}" if ( ! objId.StartsWith( " /Forum/ShowTopic/ " )) base .AddObject(objId, o, expire); LLManager.Set(objId, o, expire); RecordLog(objId, " set " ); } /// <summary> /// 移除指定ID的对象 /// </summary> /// <param name="objId"></param> public override void RemoveObject( string objId) { // 先移除本地cached,然后再移除memcached中的相应数据 base .RemoveObject(objId); LLManager.Delete(objId); Discuz.EntLib.SyncCache.SyncRemoteCache(objId); } /// <summary> /// 获取指定 key 的对象 /// </summary> /// <param name="objId"> 对象的键值 </param> public override object RetrieveObject( string objId) { object obj = base .RetrieveObject(objId); if (obj == null ) { obj = LLManager.Get(objId); if (obj != null && ! objId.StartsWith( " /Forum/ShowTopic/ " )) // 对ShowTopic页面缓存数据不放到本地缓存 { if (objId.StartsWith( " /Forum/ShowTopicGuestCachePage/ " )) // 对游客缓存页面ShowTopic数据缓存设置有效时间 base .TimeOut = GeneralConfigs.GetConfig().Guestcachepagetimeout * 60 ; if (objId.StartsWith( " /Forum/ShowForumGuestCachePage/ " )) // 对游客缓存页面ShowTopic数据缓存设置有效时间 base .TimeOut = LLServerConfigs.GetConfig().CacheShowForumCacheTime * 60 ; else base .TimeOut = LocalCacheTime; base .AddObject(objId, obj, TimeOut); } RecordLog(objId, " get " ); } return obj; } /// <summary> /// 到期时间,单位:秒 /// </summary> public override int TimeOut { get { return 3600 ; } } /// <summary> /// 本地缓存到期时间,单位:秒 /// </summary> public int LocalCacheTime { get { return LLServerConfigs.GetConfig().LocalCacheTime; } } /// <summary> /// 清空的有缓存数据 /// </summary> public override void FlushAll() { base .FlushAll(); LLManager.DeleteAll(); }}
代码比较简单,大家看一下注释就可以了。下面是LLManager.cs文件的代码:
/// <summary> /// MemCache管理操作类 /// </summary> public sealed class LLManager{ /// <summary> /// redis配置文件信息 /// </summary> private static LLServerConfigInfo llConfigInfo = LLServerConfigs.GetConfig(); /// <summary> /// 静态构造方法,初始化链接池管理对象 /// </summary> static LLManager() { } /// <summary> /// 转换 .NET 日期为 UNIX 时间戳 /// </summary> /// <param name="expire"> 到期时间,单位:秒 </param> /// <returns></returns> private static int GetExpirationUnixTime( int expire) { if (expire <= 0 ) return 0 ; DateTime expiration = DateTime.Now.AddSeconds(expire); if (expiration <= DateTime.Now) return 0 ; return Discuz.Common.UnixDateTimeHelper.ConvertToUnixTimestamp(expiration); } private static readonly BinaryFormatter bf = new BinaryFormatter(); /// <summary> /// Serialize object to buffer /// </summary> /// <param name="value"> serializable object </param> /// <returns></returns> public static byte [] Serialize( object value) { if (value == null ) return null ; var memoryStream = new MemoryStream(); memoryStream.Seek( 0 , 0 ); bf.Serialize(memoryStream, value); return memoryStream.ToArray(); } /// <summary> /// Deserialize buffer to object /// </summary> /// <param name="someBytes"> byte array to deserialize </param> /// <returns></returns> public static object Deserialize( byte [] someBytes) { if (someBytes == null ) return null ; var memoryStream = new MemoryStream(); memoryStream.Write(someBytes, 0 , someBytes.Length); memoryStream.Seek( 0 , 0 ); return bf.Deserialize(memoryStream); } public static string ToBase64( byte [] binBuffer) { int base64ArraySize = ( int )Math.Ceiling(binBuffer.Length / 3d) * 4 ; char [] charBuffer = new char [base64ArraySize]; Convert.ToBase64CharArray(binBuffer, 0 , binBuffer.Length, charBuffer, 0 ); return new string (charBuffer); } /// <summary> /// 将Base64编码文本转换成Byte[] /// </summary> /// <param name="base64"> Base64编码文本 </param> /// <returns></returns> public static Byte[] Base64ToBytes( string base64) { char [] charBuffer = base64.ToCharArray(); return Convert.FromBase64CharArray(charBuffer, 0 , charBuffer.Length); } /// <summary> /// 获取指定 key 的对象 /// </summary> /// <param name="t"> 对象的键值 </param> /// <param name="objId"> 对象的键值 </param> public static object Get( string objId) { string result = Utils.GetHttpWebResponse(llConfigInfo.ServerList + " opt=get&charset=utf-8&key= " + objId, " GET " , null ); if (result == null || result.EndsWith( " ERROR " )) return null ; else return Deserialize(Base64ToBytes(result.Substring( 0 , result.IndexOf( " $$END$$ " )))); } /// <summary> /// 设置对象到缓存中 /// </summary> /// <param name="objId"> 对象的键值 </param> /// <param name="data"> 缓存的对象 </param> public static bool Set( string objId, object data) { return Set(objId, data, 0 ); } /// <summary> /// 设置对象到缓存中 /// </summary> /// <param name="objId"> 对象的键值 </param> /// <param name="o"> 缓存的对象 </param> /// <param name="exptime"> 到期时间,单位:秒 </param> public static bool Set( string objId, object data, int exptime) { exptime = GetExpirationUnixTime(exptime); string result = Utils.UrlEncode(ToBase64(Serialize(data))) + " $$END$$ " ; result = Utils.GetHttpWebResponse( string .Format( " {0}opt=put&charset=utf-8&key={1}{2}&length={3} " , llConfigInfo.ServerList, objId, exptime > 0 ? " &exptime= " + exptime : "" , result.Length), " POST " , result); return result != null && ! result.EndsWith( " ERROR " ); } /// <summary> /// 客户端缓存操作对象 /// </summary> public static bool Delete( string objId) { string result = Utils.GetHttpWebResponse(llConfigInfo.ServerList + " opt=delete&charset=utf-8&key= " + objId, " GET " , null ); return result != null && ! result.EndsWith( " ERROR " ); } /// <summary> /// 获取所有对象,暂时未实现非http协议功能 /// </summary> public static string GetAll() { string result = Utils.GetHttpWebResponse(llConfigInfo.ServerList + " opt=getlist&charset=utf-8 " , " GET " , null ); if (result == null || result.EndsWith( " ERROR " )) return null ; else return result; } /// <summary> /// 删除所有缓存对象 /// </summary> public static bool DeleteAll() { string result = Utils.GetHttpWebResponse(llConfigInfo.ServerList + " opt=deleteall&charset=utf-8 " , " GET " , null ); if (result == null || result.EndsWith( " ERROR " )) return false ; else return true ; } } LLManager.cs类主要是以封装了以http协议方式访问llserver的操作(因为llserver可支持http协议和memcached socket协议)。该类有两个地方需要注意:
1.使用base64对value部分进行编码,以解决object对象二进制序列化后llserver无法存储的问题(llserver这个问题将会在后续版本中解决) 2.在set/get操作时,对value结尾添加“$$END$$”标识来告之数据在该标识位结束。 了解了这些内容之后,最后再说一个企业版中使用memcached socket协议连接llserver的情况。因为之前企业版中已使用了memcached,这里如果要使用llserver,只须关闭当前llserver.config文件中的
< ApplyLLServer > false </ ApplyLLServer > 并开启memcached.config文件中的
< ApplyMemCached > true </ ApplyMemCached > 选项即可,但同时也要设置该文件的“<ApplyBase64>true</ApplyBase64>”节点,这样就可以用该memcached client来链接使用llserver了。
好了,到这里今天的内容就先告一段落了。
原文链接:
作者: daizhj, 代震军
微博:
Tags: discuz!nt, memcached, redis,llserver,key/value db