在Discuz!NT中進行緩存分層(本地緩存+memcached)

       在以前的兩篇文章(Discuz!NT 緩存設計簡析, Discuz!NT中集成Memcached分佈式緩存)中,介紹了Discuz!NT中的緩存設計思路以及如何引入Memcached,當然前者是IIS進程的緩存(本地緩存),後者是分佈式內存對象緩存系統。
      兩者通過Discuz!NT中的memcached.config文件中的ApplyMemCached結點的值來決定使用哪一種緩存方式。不過在之後,有朋友反映當使用Memcached時,特別是在大併發來時,效率會打折扣,甚至有很多時間會消耗在socket套接字(創建和傳輸方面)上。而事實上也的確如此,儘管Memcached在使用池化的方式初始化一定數量的套接字資源(之前測試時實始化爲128個鏈接),在小併發(100左右)時,可能問題不大,但併發上了1000-2000時,其效率要比本地化緩存機制低1/3(loadrunner測試場景),比如loadrunner測試1000併發時,如果showtopic(顯示主題),本地緩存處理時間爲15秒,而使用memcached可能會達到25-35秒。
      顯然這是用戶所不能忍受的,所以要想解決方案。也就有了今天的文章。
      其實要解決這個問題的原理很簡單,就是將之前的兩種緩存方案(本地緩存和memcached)進行整合,原理如下:
      首先在iis進程中會將要緩存的數據緩存一份,同時也將該數據放入memcached一份,當然本地緩存的數據生命週期要比memcached少。這就造成本地緩存數據到期後,當再次訪問其則將memcached中的數據加載到本地緩存中並返回給應用程序。當緩存的數據更新時,則要更新memcached中的數據和本地緩存的數據(當然如果你要將應用程序佈署的到多個站點時,因爲不同的站點運行在不同的web園或主機上,這時你就不可以用最簡單的方式來更新其它進程和主機上的應用程序了,因爲當前緩存的數據只保存在當前web園進程中),這也就是爲什麼要給本地緩存數據設置到期時間這個值,讓其在到期後來自動從memcached獲取數據。
     原理解釋完了之後,我們來看看如何實現這個方案.
     首先,我們要看一下默認的本地緩存策略文件,其功能也就是兩年前所說的那個本地緩存策略功能,如下:
    
    /// <summary>
    
/// 默認緩存管理類
    
/// </summary>
    public class  DefaultCacheStrategy : ICacheStrategy
    {
        
private static readonly DefaultCacheStrategy instance = new DefaultCacheStrategy(); 

        
protected static volatile System.Web.Caching.Cache webCache = System.Web.HttpRuntime.Cache; 

        
/// <summary>
        
/// 默認緩存存活期爲3600秒(1小時)
        
/// </summary>
        protected int _timeOut = 3600

        
private static object syncObj = new object(); 

        
/// <summary>
        
/// 構造函數
        
/// </summary>
        static DefaultCacheStrategy()
        {}


        
/// <summary>
        
/// 設置到期相對時間[單位: 秒] 
        
/// </summary>
        public virtual int TimeOut
        {
            
set { _timeOut = value > 0 ? value : 3600; }
            
get { return _timeOut > 0 ? _timeOut : 3600; }
        } 

        
public static System.Web.Caching.Cache GetWebCacheObj
        {
            
get { return webCache; }
        }


        
/// <summary>
        
/// 加入當前對象到緩存中
        
/// </summary>
        
/// <param name="objId">對象的鍵值</param>
        
/// <param name="o">緩存的對象</param>
        public virtual void AddObject(string objId, object o)
        {    
            
if (objId == null || objId.Length == 0 || o == null)
            {
                
return;
            } 

            CacheItemRemovedCallback callBack 
= new CacheItemRemovedCallback(onRemove); 

            
if (TimeOut == 7200)
            {
                webCache.Insert(objId, o, 
null, DateTime.MaxValue, TimeSpan.Zero, System.Web.Caching.CacheItemPriority.High, callBack);
            }
            
else
            {
                webCache.Insert(objId, o, 
null, DateTime.Now.AddSeconds(TimeOut), System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.High, callBack);
            }
        } 

        
/// <summary>
        
/// 加入當前對象到緩存中
        
/// </summary>
        
/// <param name="objId">對象的鍵值</param>
        
/// <param name="o">緩存的對象</param>
        public virtual void AddObjectWith(string objId, object o)
        {
            
if (objId == null || objId.Length == 0 || o == null)
            {
                
return;
            } 

            CacheItemRemovedCallback callBack 
= new CacheItemRemovedCallback(onRemove); 

            webCache.Insert(objId, o, 
null, System.DateTime.Now.AddSeconds(TimeOut), System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.High, callBack);
        } 

        
/// <summary>
        
/// 加入當前對象到緩存中,並對相關文件建立依賴
        
/// </summary>
        
/// <param name="objId">對象的鍵值</param>
        
/// <param name="o">緩存的對象</param>
        
/// <param name="files">監視的路徑文件</param>
        public virtual void AddObjectWithFileChange(string objId, object o, string[] files)
        {
            
if (objId == null || objId.Length == 0 || o == null)
            {
                
return;
            } 

            CacheItemRemovedCallback callBack 
= new CacheItemRemovedCallback(onRemove); 

               CacheDependency dep 
= new CacheDependency(files, DateTime.Now); 

            webCache.Insert(objId, o, dep, System.DateTime.Now.AddSeconds(TimeOut), System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.High, callBack);
        } 

        
/// <summary>
        
/// 加入當前對象到緩存中,並使用依賴鍵
        
/// </summary>
        
/// <param name="objId">對象的鍵值</param>
        
/// <param name="o">緩存的對象</param>
        
/// <param name="dependKey">依賴關聯的鍵值</param>
        public virtual void AddObjectWithDepend(string objId, object o, string[] dependKey)
        {
            
if (objId == null || objId.Length == 0 || o == null)
            {
                
return;
            } 

            CacheItemRemovedCallback callBack 
= new CacheItemRemovedCallback(onRemove); 

            CacheDependency dep 
= new CacheDependency(null, dependKey, DateTime.Now); 

            webCache.Insert(objId, o, dep, System.DateTime.Now.AddSeconds(TimeOut), System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.High, callBack);
        } 

        
/// <summary>
        
/// 建立回調委託的一個實例
        
/// </summary>
        
/// <param name="key"></param>
        
/// <param name="val"></param>
        
/// <param name="reason"></param>
        public void onRemove(string key, object val, CacheItemRemovedReason reason)
        {
            
switch (reason)
            {
                
case CacheItemRemovedReason.DependencyChanged:
                    
break;
                
case CacheItemRemovedReason.Expired:
                    {
                        
//CacheItemRemovedCallback callBack = new CacheItemRemovedCallback(this.onRemove); 

                        
//webCache.Insert(key, val, null, System.DateTime.Now.AddMinutes(TimeOut),
                        
//    System.Web.Caching.Cache.NoSlidingExpiration,
                        
//    System.Web.Caching.CacheItemPriority.High,
                        
//    callBack);
                        break;
                    }
                
case CacheItemRemovedReason.Removed:
                    {
                        
break;
                    }
                
case CacheItemRemovedReason.Underused:
                    {
                        
break;
                    }
                
defaultbreak;
            }
        } 

        
/// <summary>
        
/// 刪除緩存對象
        
/// </summary>
        
/// <param name="objId">對象的關鍵字</param>
        public virtual void RemoveObject(string objId)
        {
            
if (objId == null || objId.Length == 0)
            {
                
return;
            }
            webCache.Remove(objId);
        } 

        
/// <summary>
        
/// 返回一個指定的對象
        
/// </summary>
        
/// <param name="objId">對象的關鍵字</param>
        
/// <returns>對象</returns>
        public virtual object RetrieveObject(string objId)
        {
            
if (objId == null || objId.Length == 0)
            {
                
return null;
            }            
            
return webCache.Get(objId);
        } 
}
       因爲在一開始設計Discuz!NT緩存方案時,就使用了Strategy(策略)模式,所以這裏我們只要將上面所說的改動方案以繼承的方式繼承自上面的
DefaultCacheStrategy 之後,就可以在DNTCache中使用它了。因爲之前我已經將memcached引入到了discuznt產品中,所以這裏只要改動一下已有的那個MemCachedStrategy,使其支持上面所說的緩存分佈方案即可,請看下面的代碼:
  
   /// <summary>
   
/// 企業級MemCache緩存策略類,只能使用一個web園程序
   
/// </summary>
   public class MemCachedStrategy : DefaultCacheStrategy
   {
       
/// <summary>
       
/// 添加指定ID的對象
       
/// </summary>
       
/// <param name="objId"></param>
       
/// <param name="o"></param>
       public override void AddObject(string objId, object o)
       {
           
//先向本地cached加入,然後再加到memcached
           RemoveObject(objId); 

           
base.AddObject(objId, o); 

           MemCachedManager.CacheClient.Set(objId, o);
       } 

       
/// <summary>
       
/// 添加指定ID的對象(關聯指定文件組)
       
/// </summary>
       
/// <param name="objId"></param>
       
/// <param name="o"></param>
       
/// <param name="files"></param>
       public override void AddObjectWithFileChange(string objId, object o, string[] files)
       {
           ;
       } 

       
/// <summary>
       
/// 添加指定ID的對象(關聯指定鍵值組)
       
/// </summary>
       
/// <param name="objId"></param>
       
/// <param name="o"></param>
       
/// <param name="dependKey"></param>
       public override void AddObjectWithDepend(string objId, object o, string[] dependKey)
       {
           ;
       } 

       
/// <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);
       } 

       
/// <summary>
       
/// 返回指定ID的對象
       
/// </summary>
       
/// <param name="objId"></param>
       
/// <returns></returns>
       public override object RetrieveObject(string objId)
       {
           
object obj = base.RetrieveObject(objId);
           
if (obj == null)
           {
               obj 
= MemCachedManager.CacheClient.Get(objId);
               
if (obj != null)
                   
base.AddObject(objId, obj);
           } 

           
return obj;
       } 

       
/// <summary>
       
/// 到期時間
       
/// </summary>
       public override int TimeOut 
       { 
           
get 
           { 
               
return MemCachedConfigs.GetConfig().LocalCacheTime; 
           } 
       }
   }
   
      注:MemCachedStrategy 原來已實現了ICacheStrategy接口,參見這篇文章
      這樣,我們還是可以通過memcached.config中的ApplyMemCached來判斷是否使用本地緩存方案還是當前的緩存分層方案。當然原有的memcache.config中還有添加一下屬性用於記錄當使用緩存分層方案之後的本地緩存的緩存數據時間,以向上面的類屬性TimeOut注入相應參數信息。
這樣memcached.config的內容就會變成這個樣子(本地測試配置):
 
<?xml version="1.0"?>
<MemCachedConfigInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance%22 xmlns:xsd="http://www.w3.org/2001/XMLSchema%22>
    
<ApplyMemCached>true</ApplyMemCached>  
    
<ServerList>10.0.2.137:11211</ServerList>
    
<PoolName>DiscuzNT_MemCache</PoolName>
    
<IntConnections>128</IntConnections>
    
<MinConnections>128</MinConnections>
    
<MaxConnections>512</MaxConnections>
    
<SocketConnectTimeout>1000</SocketConnectTimeout>
    
<SocketTimeout>3000</SocketTimeout>
    
<MaintenanceSleep>30</MaintenanceSleep>
    
<FailOver>true</FailOver>
    
<Nagle>true</Nagle>
    
<LocalCacheTime>60</LocalCacheTime>
</MemCachedConfigInfo>
    這樣,當使用Lr測試時,其在併發1000的情況下與使用本地緩存方案的響應時間基本穩定在15秒左右,想一下大家就會明白了,因爲在數據首次加載並進行緩存時(本地和memcached都會緩存一份,參見上面的實現代碼)。當再次訪問時,如在60秒的數據有效期內,僅訪問本地緩存,只有在數據過期時間,纔會運行再次加載數據的工作,而這種加載也只是從memcached中獲得數據,這裏我們可以暫時將memcached中的數據想像是永不過期,這樣就可以減少對database的訪問壓力,因爲這時相對於本地緩存而言,memcached已經變成了一個‘緩存數據庫’了:
      
       public override object RetrieveObject(string objId)
       {
           
object obj = base.RetrieveObject(objId);
           
if (obj == null)
           {
               obj 
= MemCachedManager.CacheClient.Get(objId);
               
if (obj != null)
                   
base.AddObject(objId, obj);
           } 

           
return obj;
       }
     現在用兩張圖再對比說明之前的memcached與現在的緩存分層方案:
        memcached
 
     改進後:
twotie-memcached
 
     總結:其實在大網站的數據緩存方案中,往往會將大量的數據(不經常變化或對時效性要求不強,但卻需頻繁訪問的數據)放入到緩存中,以此來降低數據庫的負載。本地緩存數據的時效性和穩定性受制於IIS進程中線程的運行情況,資源的佔用等因素影響,可以說數據的穩定性(不易丟失)遠不如memcached,所以這種分層方案可以有效的解決這個問題,當然這種做法還有一些其它方面的好處,就不一一說明了。
 
 

0

收藏

daizhenjun

142篇文章,50W+人氣,0粉絲

Ctrl+Enter 發佈

發佈

取消

0

分享
daizhenjun
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章