Discuz!NT跨站缓存同步

参考文章:
      Discuz!NT 缓存设计简析 
      Discuz!NT 中集成Memcached分布式缓存   
      在 Discuz!NT中进行缓存分层(本地缓存+memcached)   

      在以前的文章中,提到了在Discuz!NT中进行缓存分层的概念。以前在产品中也实现了其中的构想,但该方案有一个问题,就是若是将产品进行分布式布署 以后,若是某一站点发生数据变化时,只能更新本地缓存和Memcached缓存信息,而其它分布式布署的站点则没法收到缓存数据已修改的‘通知’,致使数 据不一样步而成为‘脏数据’。
      虽然在以前的文章中提到经过将本地缓存失效时间‘缩短’(好比15秒后即失效),以便在相对较短的时间内让本地数据失效从而再次从Memcached读取 最新的数据,但这一定不符合咱们设计的基本思路,而且致使程序的运行效率低,同时会形成过于频繁的访问Memcached,无形中增长了与 Memcached的socket开销。因此才有了今天的这篇文章。

      首先要说明的是,这个方案只有Discuz!NT的企业版(EntLib)中提供,因此在普通的版本中是找不到它的影子的,下面我就简要说明一下其实现思 路。
      由于在传统的WEB应用开发中,通常都是采用get的方式来得到所须要的数据,也就是经过从客户端向服务端发送get请求来得到数据。而若是要实现下面的 流程:     html

     当本地缓存数据变化-->更新memcached-->(通知notify)其它分布式应用   

      这里我借助主动发送的模式来实现,为了便于理解,我这里将memcached变成服务端,将分布式布署的应用当作是一个个‘客户端’,而当‘客户端’将数 据更新到memcached时,经过发送http通知的方式来通知其它‘客户端’,因此咱们要实现的代码包括两部分,一部分实现上面流程中的‘将本地数据 变化告之到memcached’。这块代码已在以前的文章中被实现了,而咱们只要在相应的‘RemoveObject’方法后跟上一行‘通知其它分布式应 用’便可(代码位于Discuz.EntLib\Memcached\MemCachedStrategy.cs),以下面所示:  web

代码缓存

         ///   <summary>
        
///  移除指 定ID的对象
        
///   </summary>
        
///   <param name="objId"></param>
         public   override   void  RemoveObject( string  objId)
        {
            
// 先移除本地cached,而后再移除memcached中的相应数据
             if  ( base .RetrieveObject(objId)  !=   null )
                
base .RemoveObject(objId);

            
if  (MemCachedManager.CacheClient.KeyExists(objId))
                MemCachedManager.CacheClient.Delete(objId);

            Discuz.EntLib.SyncCache.SyncRemoteCache(objId);
// 通知其它分布式应用
        }    

 

      下面就是‘同步其它分布式应用缓存数据’的代码了。在介绍代码以前,先要将‘发送缓存数据修改通知’的设计思想介绍一下:     安全

1.首先咱们须要一下记录着分布式 布署应用的网站列表,它主要是一连接串,好比下面这个格式(用逗号分割):      服务器

< SiteUrl > http://10.0.2.137:8088/tools/,http://10.0.2.150:8088/tools/,http://10.0.2.136:8088/tools/ </ SiteUrl >

        咱们须要将上面的连接串分割以后加上相应的更新缓存工具页面(稍后介绍)来实现移除(至关时同步)的功能。    多线程

2.为了安全起见,在发送通知的请求时,需 要对请求进行加密,以避免该功能被其它恶意代码利用,从而形成系统安全性和效率受到影响,因此我这里提供了认证码,即:mvc

< AuthCode > 123123 </ AuthCode >

       这样,验证码加密的请求只有在被同步工具正确解析后,才会更新相应的缓存数据。负载均衡

      了解这些内容以后,咱们看一下相应的实现代码以验证一下所说的设计思想(Discuz.EntLib\SyncLocalCache \SyncCache.cs):      框架

 

代码
///   <summary>
///  同步缓存类
///   </summary>
public   class  SyncCache
{
    
///   <summary>
    
///  除本站 以外的负载均衡站点列表
    
///   </summary>
     static  List < string >  syncCacheUrlList  =   null ;

    
static  LoadBalanceConfigInfo loadBalanceConfigInfo  =  LoadBalanceConfigs.GetConfig();

    
static  SyncCache()
    {
        syncCacheUrlList 
=   new  List < string > ();
        syncCacheUrlList.AddRange(loadBalanceConfigInfo.SiteUrl.
            Replace(
" tools/ " " tools/SyncLocalCache.ashx " ).Split( ' , ' ));
     
        
int  port  =  HttpContext.Current.Request.Url.Port;
        
string  localUrl  =   string .Format( " {0}://{1}{2}{3} " ,
                                         HttpContext.Current.Request.Url.Scheme,
                                         HttpContext.Current.Request.Url.Host,
                                         (port 
==   80   ||  port  ==   0 ?   ""  :  " : "   +  port,
                                         BaseConfigs.GetForumPath);

        Predicate
< string >  matchUrl  =   new  Predicate < string >
        (
            
delegate ( string  webUrl)
            {
                
return  webUrl.IndexOf(localUrl)  >=   0 // 移除本地站点连接,由于当前站点缓存已被移除。
            }
        );

        syncCacheUrlList.RemoveAll(matchUrl);
    }

      首先咱们在静态构造方法中读取相应url连接列表(loadBalanceConfigInfo配置文件),而后将其中的本地应用连接去掉,这样就不会造 成反复更新本地缓存数据(从而形成死循环)的问题了。接着就是使用一个线程来发送相应的同步数据请求到各个分布式应用上,以下(包括使用认证码加密连接信 息):  asp.net

 代码

   ///   <summary>
    
///  同步远 程缓存信息
    
///   </summary>
    
///   <param name="cacheKey"></param>
     public   static   void  SyncRemoteCache( string  cacheKey)
    {
        
foreach  ( string  webSite  in  syncCacheUrlList)
        {
            
string  url  =   string .Format( " {0}?cacheKey={1}&passKey={2} " ,
                                       webSite,
                                       cacheKey,
                                       Discuz.Common.Utils.UrlEncode(Discuz.Common.DES.Encode(cacheKey, loadBalanceConfigInfo.AuthCode)));

            ThreadSyncRemoteCache src 
=   new  ThreadSyncRemoteCache(url);
            
new  Thread( new  ThreadStart(src.Send)).Start();
        }
    }

 

      这里咱们使用线程方式来更新相应的分布式应用,思路是: 

      对一个分布式应用发送三次请求,若是其中某一次返回结果为ok时,则再也不向其发送其他请求了。若是上一次请求不成功,则当前线程暂停五秒后再次发送请求, 直到三次请求用完为止。这样主要是考虑到远程应用上的主机可能某一时刻处于忙碌状态而没法响应,因此采用发送三次(每次间隔五秒)的方式。

      下面就是它的主要实现代码:       

 

代码
     ///   <summary>
    
///  多线程 更新远程缓存
    
///   </summary>
     public   class  ThreadSyncRemoteCache
    {
        
public   string  _url;

        
public  ThreadSyncRemoteCache( string  url)
        {
            _url 
=  url;
        }

        
public   void  Send()
        {
            
try
            {
                
// 设置循环三次,若是 某一次更新成功("OK"),则跳出循环
                 for  ( int  count  =   0 ; count  <   3 ; count ++ )
                {
                    
if  ( this .SendWebRequest(_url)  ==   " OK " )
                        
break ;
                    
else
                        Thread.Sleep(
5000 ); // 若是更新不成功,则暂停5秒后再次更新
                }
            }
            
catch  { }
            
finally
            {
                
if  (Thread.CurrentThread.IsAlive)
                    Thread.CurrentThread.Abort();                  
            }
       }

        
///   <summary>
        
///  发送 web请求
        
///   </summary>
        
///   <param name="url"></param>
        
///   <returns></returns>
         public   string  SendWebRequest( string  url)
        {
            StringBuilder builder 
=   new  StringBuilder();
            
try
            {
                WebRequest request 
=  WebRequest.Create( new  Uri(url));
                request.Method 
=   " GET " ;
                request.Timeout 
=   15000 ;
                request.ContentType 
=   " Text/XML " ;
                
using  (WebResponse response  =  request.GetResponse())
                {
                    
using  (StreamReader reader  =   new  StreamReader(response.GetResponseStream(), Encoding.UTF8))
                    {
                        builder.Append(reader.ReadToEnd());
                    }
                }
            }
            
catch
            {
                builder.Append(
" Process Failed! " );
            }
            
return  builder.ToString();
        }
    }
 

 

     如今发送请求的功能介绍完了,下面简要介绍一下在‘分布式应用’那一方如何对上面发送的请求进行解析操做的。请看下面的代码段:    

 

代码
///   <summary>
    
///  同步本 地缓存
    
///   </summary>
    [WebService(Namespace  =   " http://tempuri.org/ " )]
    [WebServiceBinding(ConformsTo 
=  WsiProfiles.BasicProfile1_1)]
    
public   class  SyncLocalCache : IHttpHandler
    {
        
public   void  Proce***equest(HttpContext context)
        {
            context.Response.ContentType 
=   " text/plain " ;
            
string  cacheKey  =  context.Request.QueryString[ " cacheKey " ];
            
string  passKey  =  context.Request.QueryString[ " passKey " ];

            
if  (Utils.StrIsNullOrEmpty(cacheKey))
            {
                context.Response.Write(
" CacheKey is not null! " );
                
return ;
            }
            
if  ( ! cacheKey.StartsWith( " /Forum " ))
            {
                context.Response.Write(
" CacheKey is not valid! " );
                
return ;
            }
            
if  (passKey  !=  Discuz.Common.DES.Encode(cacheKey, Discuz.Config.LoadBalanceConfigs.GetConfig().AuthCode))
            {
                context.Response.Write(
" AuthCode is not valid! " );
                
return ;
            }

            
// 更新本地缓存 (注:此处不可以使用MemCachedStrategy的RemoveObject方法,由于该方法中有SyncRemoteCache的调用,会形成循 环调用)
            Discuz.Cache.DNTCache cache  =  Discuz.Cache.DNTCache.GetCacheService();
            cache.LoadCacheStrategy(
new  DefaultCacheStrategy());
            cache.RemoveObject(cacheKey);
            cache.LoadDefaultCacheStrategy();    

            context.Response.Write(
" OK " );
        }

        
public   bool  IsReusable
        {
            
get
            {
                
return   false ;
            }
        }
    }
 

          上面代码首先会获取请求过来的缓存键值和passKey(即认证码加密后的连接),而后在本地进行数据有效性校验,若是认证经过的 话,就能够对其要移除的缓存数据进行操做了,并在操做成功以后返回ok信息。该页面采用synclocalcache.ashx文件进行声明,

如 下: 

 

< %@ WebHandler  Language ="C#"   Class ="Discuz.EntLib.SyncLocalCache"  % >

 

     到这里,只要将该ashx文件放到站点的tools/文件夹下,就能够实现跨站同步缓存数据的功能了。目前考虑的场景仍是比较单一的,因此实现的 代码也相对简单,不排除随着业务逻辑复杂度不断提高而作从新设计的可能性。

     为了便于购买咱们商业服务的客户进行管理操做,咱们还提供了一个企业级的监控管理工具,该工具基本asp.net mvc框架开发,提供了监视负 载均衡,同步缓存,读写分离检查和远程服务器运行状态(CPU,内存等使用状况)。下面是该工具所提供的同步缓存数据的功能界面: 

      该工具的开发思想和实现原理会在后面章节中加以详细说明,敬请关注:)     

      原文连接: http://www.cnblogs.com/daizhj/archive/2010/06/18/discuznt_memcache_syncdata.html

      做者: daizhj, 代震军

      Tags: discuz!nt,memcached,分层

      网址: http://daizhj.cnblogs.com/