作為一個社群類型軟體,大并發支援和高效穩定運作永遠是“硬道理”,而有效安全的使用緩存恰恰能起到事倍功半的效果。而.NET本身所提供的緩存機制又顯得過于“單薄”,比如說訂制不太靈活友善, 緩存對象之間層次感不強, 使用時缺乏統一的管理等等。
Discuz!NT緩存産生背景:
在去年五月份我加入Discuz!NT項目組時,發現這個項目當時還未使用緩存機制。主要原因是項目還處于起步階段,很多東西還隻是有想法,但未付諸實施,或還沒找到合适的方案, 而緩存就是其中一個到底該不該使用,如果使用的該到底能多大程度緩解資料庫壓力以及開發成本的東西。
我當時正好有一個比較好的“原型”(從一本書上看到的源碼),也就是今天Discuz!NT所使用的緩存機制的雛形,但當時它在功能上還很不健全且存在一些“緻命的” BUG, 但實作簡單的緩存資料對象還是綽綽有餘的,于是我通過一個簡單的測試用例(緩存資料表和StringBuilder對象)和雪人一起讨論并分析後得到一些資料,基本上肯定了使用緩存解決對資料庫象中經常通路但又不經常更新的資料進行緩存的使用方案,同時也要求這個緩存機制要使用起來盡可能的簡單,同時功能擴充要非常友善。
是以本人就在這個“原型”的基本上進行了一段時間的功能擴充和BUG修正才得到今天大家所看到的這部分代碼。
現在将Discuz!NT的緩存架構說明如下,先請大家看一下Discuz!NT架構圖:
其實這個構架說白了就是一個标準的“政策”模式,為了對比友善,我把政策模式的結構圖放在下面:
裡面的DNTCache就是“政策”模式的應用場景,而DefaultCache , ForumCache,RssCache等等就是相應的具體政策,每一種政策都會對.net所提供的緩存機制進行一番“訂制”,以實作不同的用途。比如系統DefaultCache在對象到期時提供資料再次加載機制,而ForumCache不使用這種機制,另外還有緩存的到期時間幾種政策也各不相同,這都是根據具體的應用場景"量身訂制"的。
說到這裡,您所要做的就是下載下傳一份源碼按上圖索骥就可以把整個緩存機制搞清楚。
下面對緩存設計所采用的幾種技術做一下簡要說明。包括XML,XPATH ,"單件模式" 以及跨web園共享資料。
首先請看一下代碼:(xml xpath)
1 //要存取的xpath格式路徑
2 //要緩存的對象
3 public virtual void AddObject(string xpath, object o ,string[] files)
4 {
5
6 //整理XPATH表達式資訊
7 string newXpath = PrepareXpath(xpath);
8 int separator = newXpath.LastIndexOf("/");
9 //找到相關的組名
10 string group = newXpath.Substring(0,separator );
11 //找到相關的對象
12 string element = newXpath.Substring(separator + 1);
13
14 XmlNode groupNode = objectXmlMap.SelectSingleNode(group);
15 //建立對象的唯一鍵值, 用以映射XML和緩存對象的鍵
16 string objectId="";
17
18 XmlNode node = objectXmlMap.SelectSingleNode(PrepareXpath(xpath));
19 if ( node != null)
20 {
21 objectId = node.Attributes["objectId"].Value;
22 }
23 if(objectId=="")
24 {
25 groupNode = CreateNode(group);
26 objectId= Guid.NewGuid().ToString();
27 //建立新元素和一個屬性 for this perticular object
28 XmlElement objectElement = objectXmlMap.OwnerDocument.CreateElement(element);
29 XmlAttribute objectAttribute =objectXmlMap.OwnerDocument.CreateAttribute("objectId");
30 objectAttribute.Value = objectId;
31 objectElement.Attributes.Append(objectAttribute);
32 //為XML文檔建立新元素
33 groupNode.AppendChild(objectElement);
34 }
35 else
36 {
37 //建立新元素和一個屬性 for this perticular object
38 XmlElement objectElement = objectXmlMap.OwnerDocument.CreateElement(element);
39 XmlAttribute objectAttribute =objectXmlMap.OwnerDocument.CreateAttribute("objectId");
40 objectAttribute.Value = objectId;
41 objectElement.Attributes.Append(objectAttribute);
42 //為XML文檔建立新元素
43 groupNode.ReplaceChild(objectElement,node);
44 }
45 //向緩存加入新的對象
46 cs.AddObjectWithFileChange(objectId,o,files);
47
48 }
49
為什麼要用XML, 主要是為了使用XML中的階層化功能以及相關的結點添加,替換,移除,還有就是當希望對緩存的結構資訊進行“持久化”操作時會很友善等。
XPATH 便于能過層次表達式(hierarchical expression) 對XML檔案進行查找搜尋。
通過上面或其它的類似代碼,就可以建構起一個xml樹來管理已加入到系統的緩存對象了。
使用"單件模式"模式生成全局唯一的“應用場景”,因為緩存這種東西通常在存儲共享資料時它的效果最好,編碼也最容易實作和管理,同時項目本身基本上就是對經常通路但不經常改變的資料庫資料(可看成是共享資料)進行緩存,是以使用單件模式就順理成章了。
請看如下代碼:
public static DNTCache GetCacheService()
{
if (instance == null)
{
lock (lockHelper)
{
if (instance == null)
{
instance = new DNTCache();
}
}
}
//檢查并移除相應的緩存項
//注:此處代碼為即将釋出的2.0版本中的代碼類,如果您想了解其中
//的代碼可參見開源版本中的Discuz.Forum.cachefactory.cs檔案中
//相應函數
instance=CachesFileMonitor.CheckAndRemoveCache(instance);
return instance;
}
小插曲:
1.項目到了beta版時出現了無法跨web園共享資料的問題。它的表現是這樣的,當你在IIS服務的應用程式池中設定2個或以上的WEB園時,這時你在背景更新緩存時,就是出現緩存“隔三差五”資料不更新或輪換更新的情況。說白了,就是隻有一個應用程序中的資料緩存被更新,而其餘的程序中所有資料還沒事人似的保留原有的面貌。這個問題主要是因為static的資料執行個體(也就是上面所有的單體代碼中的對象)雖然而目前程序中“唯一”,但在其它程序中卻各自都有一個造成的。一開始我也很驚訝,為什麼微軟不能像提供“全局”鈎子那樣的技術一樣提供一種跨WEB園來共享資料的技術或關鍵字呢,不過一轉念也猜出了一二分,必定多WEB園是一種讓程式(WEB)跑起來更加安全,穩定快速的“解決方案”。 因為誰都不好說自己的程式一點BUG沒有,即有真有這樣的代碼,但當遇上運作環境這個因素後,也會表現得有些難以控制。
但微軟通過web園這個技術就會把運作在幾個不同程序下的程式互相隔離,使其誰也不影響到誰,即使其中一個程序down了,而其它程序依就會繼續正常 "工作" 。是以程式中的對象執行個體和所有
資源每個程序中都會儲存一份,完全相同。而如果引用共享機制就有可能出現當程序共享的資料
或程式對象出現問題時,所有程序就可能都玩完了, 是以就需要程序隔離。
說是這麼說,但總也要想個辦法解決當時面臨的問題吧。記得在豪傑工作期間,一次老梁給我們開會,其中的一段話我至今還記憶猶新,他說CPU通路記憶體的速度和通路硬碟的速度在某些情況下是相近的,如果我沒了解的話比如說“虛拟緩存”或最新頻繁通路的硬碟區段,這些地方的代碼或檔案會有比較高的運作和通路效率。是以,我想到了使用檔案标志關聯的方法來解決這個多程序問題。接着就順理成章的使用了檔案修改日期這個屬性進行在多程序下緩存是否更新的依據了,大家可以到開源下載下傳包中的config檔案夾下把一個cache.config的檔案,對應最新的資料項再回過頭來看如下代碼就會一清二楚了:
public static DNTCache CheckAndRemoveCache(DNTCache instance)//
//當程式運作中cache.config發生變化時則對緩存對象做删除的操作
cachefilenewchange = System.IO.File.GetLastWriteTime(path);
if (cachefileoldchange != cachefilenewchange)
{
lock (cachelockHelper)
{
if (cachefileoldchange != cachefilenewchange)
{
//當有要清除的項時
DataSet dsSrc = new DataSet();
dsSrc.ReadXml(path);
foreach (DataRow dr in dsSrc.Tables[0].Rows)
{
if (dr["xpath"].ToString().Trim() != "")
{
DateTime removedatetime = DateTime.Now;
try
{
removedatetime = Convert.ToDateTime(dr["removedatetime"].
ToString().Trim());
}
catch {;}
if (removedatetime > cachefilenewchange.AddSeconds(-2))
string xpath = dr["xpath"].ToString().Trim();
instance.RemoveObject(xpath, false);
}
}
cachefileoldchange = cachefilenewchange;
dsSrc.Dispose();
}
}
}
return instance;
2.另外需要說明的是在4月份時緩存機制出現了一些問題,比如緩存資料丢失以及在.net2下
的死循環的問題,後來在雪人的建議下采用每個緩存都有緩存标志來解決資料丢失的問題。也就
是如下的代碼段:
1 //添加時
2 public virtual void AddObject(string xpath, DataTable dt)
3 {
4 lock(lockHelper)
5 {
6 if(dt.Rows.Count>0)
7 {
8 AddObject(xpath+"flag", CacheFlag.CacheHaveData);
9 }
10 else
11 {
12 AddObject(xpath+"flag", CacheFlag.CacheNoData);
13 }
14 AddObject(xpath, (object) dt);
15 }
16 }
18
19 //擷取時
20 public virtual object RetrieveObject(string xpath)
21 {
22 try
23 {
24 object cacheObject = RetrieveOriginObject(xpath);
25 CacheFlag cf = (CacheFlag) RetrieveOriginObject(xpath+"flag");
26
27 //當标志位中有資料時
28 if(cf ==CacheFlag.CacheHaveData)
29 {
30 string otype = cacheObject.GetType().Name.ToString();
31
32 //當緩存類型是資料表類型時
33 if(otype.IndexOf("Table")>0)
34 {
35 System.Data.DataTable dt = cacheObject as DataTable;
36 if ((dt == null) || (dt.Rows.Count == 0))
37 {
38 return null;
39 }
40 else
41 {
42 return cacheObject;
43 }
44 }
45
46 }
47
而死循環的問題主要是因為.net2下的緩存回調加載機制和程式本身的一個BUG造成的,目前已修正, 大家請放心使用。
目前已開發但還未使用的功能:
1.一鍵多值:請看DNTCache代碼段中的AddMultiObjects(string xpath,object[] objValue),擷取時使用object[] RetrieveObjectList(string xpath)方法傳回即可,這樣就可以用一個xpath來存取一組對象了。
它的實作代碼也相對簡單,這裡就不多說了,隻把代碼貼在此處。
public virtual bool AddMultiObjects(string xpath,object[] objValue)
{
lock(lockHelper)
//RemoveMultiObjects(xpath);
if (xpath != null && xpath != "" && xpath.Length != 0 && objValue != null)
for (int i = 0; i < objValue.Length; i++)
AddObject(xpath + "/Multi/_" + i.ToString(),objValue[i]);
return true;
return false;
2.批量移除緩存
它主要是利用XML有按路徑層次存儲的特點才這樣做的,主要是去掉位于目前路徑下的所有子結點的緩存資料。
它的函數聲明如下:RemoveObject(string xpath, bool writeconfig)
它的實作代碼也相對簡單,這裡就不多說了, 隻把代碼貼在此處。
1 public virtual void RemoveObject(string xpath, bool writeconfig)
2 {
3 lock(lockHelper)
4 {
5 try
6 {
7 if(writeconfig)
8 {
9 CachesFileMonitor.UpdateCacheItem(xpath);
10 }
11
12 XmlNode result = objectXmlMap.SelectSingleNode(PrepareXpath(xpath));
13 //檢查路徑是否指向一個組或一個被緩存的執行個體元素
14 if (result.HasChildNodes)
15 {
16 //删除所有對象和子結點的資訊
17 XmlNodeList objects = result.SelectNodes("*[@objectId]");
18 string objectId = "";
19 foreach (XmlNode node in objects)
20 {
21 objectId = node.Attributes["objectId"].Value;
22 node.ParentNode.RemoveChild(node);
23 //删除對象
24 cs.RemoveObject(objectId);
25 }
26 }
27 else
28 {
29 //删除元素結點和相關的對象
30 string objectId = result.Attributes["objectId"].Value;
31 result.ParentNode.RemoveChild(result);
32 cs.RemoveObject(objectId);
33 }
34
35 //檢查并移除相應的緩存項
36 }
37 catch
38 { //如出錯誤表明目前路徑不存在
39 }
40 }
41 }
42
43
已開發出來,但卻去掉了的功能。
在正式版出現之前,背景管理中有記錄緩存日志的功能,它的實作方式是基于"通路者"模式實作的(大家應該可以在項目中找到這個類LogVisitor)。但因為後來不少站長反映日志表操作的過于頻繁導緻日志記錄急劇增加,而把這部分功能拿下了。我在這裡說出來就是想給大家提個醒,對于新功能或新技術的追求要非常謹慎,要不就會出現您費盡千辛萬苦開發的功能,最後卻沒人買帳就郁悶了。
最後需要說明的就是,為什麼要先把這塊功能先發到園子裡來。因為我們産品的Discuz!NT2.0産品即将釋出,而整個産品的架構也出現了不少變化,而由于緩存結構相對穩定,是以變化的不大。這才在今天發個BLOG講給大家的,下一篇關于DISCUZ!NT架構的文章要等到正式版釋出之後了。到時大家下載下傳代碼之後再對照新代碼給大家聊聊這個産品的其它設計思路(按我的了解)。