參考文章:
Discuz!NT 緩存設計簡析
Discuz!NT 中集成Memcached分佈式緩存
在 Discuz!NT中進行緩存分層(本地緩存+memcached)
在之前的文章中,提到了在Discuz!NT中進行緩存分層的概念。之前在產品中也實現了其中的構想,但該方案有一個問題,就是如果將產品進行分佈式佈署 之後,如果某一站點發生數據變化時,只能更新本地緩存和Memcached緩存信息,而其它分佈式佈署的站點則無法收到緩存數據已修改的‘通知’,導致數 據不同步而成爲‘髒數據’。
雖然在之前的文章中提到通過將本地緩存失效時間‘縮短’(比如15秒後即失效),以便在相對較短的時間內讓本地數據失效從而再次從Memcached讀取 最新的數據,但這必定不符合我們設計的基本思路,並且導致程序的運行效率低,同時會造成過於頻繁的訪問Memcached,無形中增加了與 Memcached的socket開銷。所以纔有了今天的這篇文章。
首先要說明的是,這個方案只有Discuz!NT的企業版(EntLib)中提供,所以在普通的版本中是找不到它的影子的,下面我就簡要說明一下其實現思 路。
因爲在傳統的WEB應用開發中,一般都是採用get的方式來獲得所需要的數據,也就是通過從客戶端向服務端發送get請求來獲得數據。而如果要實現下面的 流程:
這裏我藉助主動發送的模式來實現,爲了便於理解,我這裏將memcached變成服務端,將分佈式佈署的應用看成是一個個‘客戶端’,而當‘客戶端’將數 據更新到memcached時,通過發送http通知的方式來通知其它‘客戶端’,所以我們要實現的代碼包括兩部分,一部分實現上面流程中的‘將本地數據 變化告之到memcached’。這塊代碼已在之前的文章中被實現了,而我們只要在相應的‘RemoveObject’方法後跟上一行‘通知其它分佈式應 用’即可(代碼位於Discuz.EntLib\Memcached\MemCachedStrategy.cs),如下面所示:
代碼
/// 移除指 定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.首先我們需要一下記錄着分佈式 佈署應用的網站列表,它主要是一鏈接串,比如下面這個格式(用逗號分割):
我們需要將上面的鏈接串分割之後加上相應的更新緩存工具頁面(稍後介紹)來實現移除(相當時同步)的功能。
2.爲了安全起見,在發送通知的請求時,需 要對請求進行加密,以免該功能被其它惡意代碼利用,從而造成系統安全性和效率受到影響,所以我這裏提供了認證碼,即:
這樣,驗證碼加密的請求只有在被同步工具正確解析後,纔會更新相應的緩存數據。
瞭解這些內容之後,我們看一下相應的實現代碼以驗證一下所說的設計思想(Discuz.EntLib\SyncLocalCache \SyncCache.cs):
/// 同步緩存類
/// </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配置文件),然後將其中的本地應用鏈接去掉,這樣就不會造 成反覆更新本地緩存數據(從而造成死循環)的問題了。接着就是使用一個線程來發送相應的同步數據請求到各個分佈式應用上,如下(包括使用認證碼加密鏈接信 息):
代碼
/// 同步遠 程緩存信息
/// </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>
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>
[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文件進行聲明,
如 下:
到這裏,只要將該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/