大約在兩年前我寫過一篇關于
Discuz!NT緩存架構的文章,在那篇文章的結尾介紹了在IIS中如果開啟多個
應用程序池會造成多個緩存實例之間數(shù)據(jù)同步的問題。雖然給出了一個解決方案,但無形中卻把壓力轉移到了
磁盤I/O上(多個進程并發(fā)訪問cache.config文件)。其實從那時起我就開始關注有什么更好的方案,當然今
天本文中所說的Memcached,以及Velocity等這類的分布式緩存方案之前都考慮過,但一直未能決定該使用那
個。起碼Velocity要在.net 4.0之后才會提供,雖然是原生態(tài),但有些遠水解不了近火。
我想真正等到Velocity能堪當重任還要等上一段時間。于是我就開始將注意力轉移到了Memcached,必定
有Facebook這只“超級小白鼠”使用它并且反響還不錯。所以就開始嘗試動手在產品中集成Memcached。
其實在之前的那篇關于Discuz!NT緩存架構的文章中已提到過,使用了設計模式中的“策略模式”來構造。
所以為了與以往使用緩存的代碼格式相兼容,所以這里采用新添加MemCachedStrategy(MemCached策略)
來構造一個緩存策略類以便于當管理后臺開啟“MemCached”時以“MemCached策略模式”來做為當前系統(tǒng)默認
的策略模式。
其代碼段如下(
Discuz.Cache/MemCached.cs):
/// <summary>
/// MemCache緩存策略類
/// </summary>
public class MemCachedStrategy : Discuz.Cache.ICacheStrategy
{
/// <summary>
/// 添加指定ID的對象
/// </summary>
/// <param name="objId"></param>
/// <param name="o"></param>
public void AddObject(string objId, object o)
{
RemoveObject(objId);
if (TimeOut > 0)
{
MemCachedManager.CacheClient.Set(objId, o, System.DateTime.Now.AddMinutes(TimeOut));
}
else
{
MemCachedManager.CacheClient.Set(objId, o);
}
}
/// <summary>
/// 添加指定ID的對象(關聯(lián)指定文件組)
/// </summary>
/// <param name="objId"></param>
/// <param name="o"></param>
/// <param name="files"></param>
public void AddObjectWithFileChange(string objId, object o, string[] files)
{
;
}
/// <summary>
/// 添加指定ID的對象(關聯(lián)指定鍵值組)
/// </summary>
/// <param name="objId"></param>
/// <param name="o"></param>
/// <param name="dependKey"></param>
public void AddObjectWithDepend(string objId, object o, string[] dependKey)
{
;
}
/// <summary>
/// 移除指定ID的對象
/// </summary>
/// <param name="objId"></param>
public void RemoveObject(string objId)
{
if (MemCachedManager.CacheClient.KeyExists(objId))
MemCachedManager.CacheClient.Delete(objId);
}
/// <summary>
/// 返回指定ID的對象
/// </summary>
/// <param name="objId"></param>
/// <returns></returns>
public object RetrieveObject(string objId)
{
return MemCachedManager.CacheClient.Get(objId);
}
/// <summary>
/// 到期時間
/// </summary>
public int TimeOut { set; get; }
}
上面類實現(xiàn)的接口
Discuz.Cache.ICacheStrategy定義如下:
/// <summary>
/// 公共緩存策略接口
/// </summary>
public interface ICacheStrategy
{
/// <summary>
/// 添加指定ID的對象
/// </summary>
/// <param name="objId"></param>
/// <param name="o"></param>
void AddObject(string objId, object o);
/// <summary>
/// 添加指定ID的對象(關聯(lián)指定文件組)
/// </summary>
/// <param name="objId"></param>
/// <param name="o"></param>
/// <param name="files"></param>
void AddObjectWithFileChange(string objId, object o, string[] files);
/// <summary>
/// 添加指定ID的對象(關聯(lián)指定鍵值組)
/// </summary>
/// <param name="objId"></param>
/// <param name="o"></param>
/// <param name="dependKey"></param>
void AddObjectWithDepend(string objId, object o, string[] dependKey);
/// <summary>
/// 移除指定ID的對象
/// </summary>
/// <param name="objId"></param>
void RemoveObject(string objId);
/// <summary>
/// 返回指定ID的對象
/// </summary>
/// <param name="objId"></param>
/// <returns></returns>
object RetrieveObject(string objId);
/// <summary>
/// 到期時間
/// </summary>
int TimeOut { set;get;}
}
當然在MemCachedStrategy類中還有一個對象要加以說明,就是MemCachedManager,該類主要是對
Memcached一些常操作和相關初始化實例調用的“封裝”,下面是是其變量定義和初始化構造方法的代碼:
/// <summary>
/// MemCache管理操作類
/// </summary>
public sealed class MemCachedManager
{
#region 靜態(tài)方法和屬性
private static MemcachedClient mc = null;
private static SockIOPool pool = null;
private static MemCachedConfigInfo memCachedConfigInfo = MemCachedConfigs.GetConfig();
private static string [] serverList = null;
static MemCachedManager()
{
CreateManager();
}
private static void CreateManager()
{
serverList = Utils.SplitString(memCachedConfigInfo.ServerList, ""r"n");
pool = SockIOPool.GetInstance(memCachedConfigInfo.PoolName);
pool.SetServers(serverList);
pool.InitConnections = memCachedConfigInfo.IntConnections;//初始化鏈接數(shù)
pool.MinConnections = memCachedConfigInfo.MinConnections;//最少鏈接數(shù)
pool.MaxConnections = memCachedConfigInfo.MaxConnections;//最大連接數(shù)
pool.SocketConnectTimeout = memCachedConfigInfo.SocketConnectTimeout;//Socket鏈接超時時間
pool.SocketTimeout = memCachedConfigInfo.SocketTimeout;// Socket超時時間
pool.MaintenanceSleep = memCachedConfigInfo.MaintenanceSleep;//維護線程休息時間
pool.Failover = memCachedConfigInfo.FailOver; //失效轉移(一種備份操作模式)
pool.Nagle = memCachedConfigInfo.Nagle;//是否用nagle算法啟動socket
pool.HashingAlgorithm = HashingAlgorithm.NewCompatibleHash;
pool.Initialize();
mc = new MemcachedClient();
mc.PoolName = memCachedConfigInfo.PoolName;
mc.EnableCompression = false;
}
/// <summary>
/// 緩存服務器地址列表
/// </summary>
public static string[] ServerList
{
set
{
if (value != null)
serverList = value;
}
get { return serverList; }
}
/// <summary>
/// 客戶端緩存操作對象
/// </summary>
public static MemcachedClient CacheClient
{
get
{
if (mc == null)
CreateManager();
return mc;
}
}
public static void Dispose()
{
if (pool != null)
pool.Shutdown();
}


上面代碼中構造方法會初始化一個池來管理執(zhí)行Socket鏈接,并提供靜態(tài)屬性CacheClient以便MemCachedStrategy
來調用。
當然我還在這個管理操作類中添加了幾個方法分別用于檢測當前有效的分布式緩存服務器的列表,向指定(或全部)
緩存服務器發(fā)送特定stats命令來獲取當前緩存服務器上的數(shù)據(jù)信息和內存分配信息等,相應的方法如下(詳情見注釋):
/// <summary>
/// 獲取當前緩存鍵值所存儲在的服務器
/// </summary>
/// <param name="key">當前緩存鍵</param>
/// <returns>當前緩存鍵值所存儲在的服務器</returns>
public static string GetSocketHost(string key)
{
string hostName = "";
SockIO sock = null;
try
{
sock = SockIOPool.GetInstance(memCachedConfigInfo.PoolName).GetSock(key);
if (sock != null)
{
hostName = sock.Host;
}
}
finally
{
if (sock != null)
sock.Close();
}
return hostName;
}
/// <summary>
/// 獲取有效的服務器地址
/// </summary>
/// <returns>有效的服務器地</returns>
public static string[] GetConnectedSocketHost()
{
SockIO sock = null;
string connectedHost = null;
foreach (string hostName in serverList)
{
if (!Discuz.Common.Utils.StrIsNullOrEmpty(hostName))
{
try
{
sock = SockIOPool.GetInstance(memCachedConfigInfo.PoolName).GetConnection(hostName);
if (sock != null)
{
connectedHost = Discuz.Common.Utils.MergeString(hostName, connectedHost);
}
}
finally
{
if (sock != null)
sock.Close();
}
}
}
return Discuz.Common.Utils.SplitString(connectedHost, ",");
}
/// <summary>
/// 獲取服務器端緩存的數(shù)據(jù)信息
/// </summary>
/// <returns>返回信息</returns>
public static ArrayList GetStats()
{
ArrayList arrayList = new ArrayList();
foreach (string server in serverList)
{
arrayList.Add(server);
}
return GetStats(arrayList, Stats.Default, null);
}
/// <summary>
/// 獲取服務器端緩存的數(shù)據(jù)信息
/// </summary>
/// <param name="serverArrayList">要訪問的服務列表</param>
/// <returns>返回信息</returns>
public static ArrayList GetStats(ArrayList serverArrayList, Stats statsCommand, string param)
{
ArrayList statsArray = new ArrayList();
param = Utils.StrIsNullOrEmpty(param) ? "" : param.Trim().ToLower();
string commandstr = "stats";
//轉換stats命令參數(shù)
switch (statsCommand)
{
case Stats.Reset: { commandstr = "stats reset"; break; }
case Stats.Malloc: { commandstr = "stats malloc"; break; }
case Stats.Maps: { commandstr = "stats maps"; break; }
case Stats.Sizes: { commandstr = "stats sizes"; break; }
case Stats.Slabs: { commandstr = "stats slabs"; break; }
case Stats.Items: { commandstr = "stats"; break; }
case Stats.CachedDump:
{
string[] statsparams = Utils.SplitString(param, " ");
if(statsparams.Length == 2)
if(Utils.IsNumericArray(statsparams))
commandstr = "stats cachedump " + param;
break;
}
case Stats.Detail:
{
if(string.Equals(param, "on") || string.Equals(param, "off") || string.Equals(param, "dump"))
commandstr = "stats detail " + param.Trim();
break;
}
default: { commandstr = "stats"; break; }
}
//加載返回值
Hashtable stats = MemCachedManager.CacheClient.Stats(serverArrayList, commandstr);
foreach (string key in stats.Keys)
{
statsArray.Add(key);
Hashtable values = (Hashtable)stats[key];
foreach (string key2 in values.Keys)
{
statsArray.Add(key2 + ":" + values[key2]);
}
}
return statsArray;
}
/// <summary>
/// Stats命令行參數(shù)
/// </summary>
public enum Stats
{
/// <summary>
/// stats : 顯示服務器信息, 統(tǒng)計數(shù)據(jù)等
/// </summary>
Default = 0,
/// <summary>
/// stats reset : 清空統(tǒng)計數(shù)據(jù)
/// </summary>
Reset = 1,
/// <summary>
/// stats malloc : 顯示內存分配數(shù)據(jù)
/// </summary>
Malloc = 2,
/// <summary>
/// stats maps : 顯示"/proc/self/maps"數(shù)據(jù)
/// </summary>
Maps =3,
/// <summary>
/// stats sizes
/// </summary>
Sizes = 4,
/// <summary>
/// stats slabs : 顯示各個slab的信息,包括chunk的大小,數(shù)目,使用情況等
/// </summary>
Slabs = 5,
/// <summary>
/// stats items : 顯示各個slab中item的數(shù)目和最老item的年齡(最后一次訪問距離現(xiàn)在的秒數(shù))
/// </summary>
Items = 6,
/// <summary>
/// stats cachedump slab_id limit_num : 顯示某個slab中的前 limit_num 個 key 列表
/// </summary>
CachedDump =7,
/// <summary>
/// stats detail [on|off|dump] : 設置或者顯示詳細操作記錄 on:打開詳細操作記錄 off:關閉詳細操作記錄 dump: 顯示詳細操作記錄(每一個鍵值get,set,hit,del的次數(shù))
/// </summary>
Detail = 8
}
當然在配置初始化緩存鏈接池時使用了配置文件方式(memcached.config)來管理相關參數(shù),其info信息
類說明如下(
Discuz.Config/MemCachedConfigInfo.cs):
/// <summary>
/// MemCached配置信息類文件
/// </summary>
public class MemCachedConfigInfo : IConfigInfo
{
private bool _applyMemCached;
/// <summary>
/// 是否應用MemCached
/// </summary>
public bool ApplyMemCached
{
get
{
return _applyMemCached;
}
set
{
_applyMemCached = value;
}
}
private string _serverList;
/// <summary>
/// 鏈接地址
/// </summary>
public string ServerList
{
get
{
return _serverList;
}
set
{
_serverList = value;
}
}
private string _poolName;
/// <summary>
/// 鏈接池名稱
/// </summary>
public string PoolName
{
get
{
return Utils.StrIsNullOrEmpty(_poolName) ? "DiscuzNT_MemCache" : _poolName;
}
set
{
_poolName = value;
}
}
private int _intConnections;
/// <summary>
/// 初始化鏈接數(shù)
/// </summary>
public int IntConnections
{
get
{
return _intConnections > 0 ? _intConnections : 3;
}
set
{
_intConnections = value;
}
}
private int _minConnections;
/// <summary>
/// 最少鏈接數(shù)
/// </summary>
public int MinConnections
{
get
{
return _minConnections > 0 ? _minConnections : 3;
}
set
{
_minConnections = value;
}
}
private int _maxConnections;
/// <summary>
/// 最大連接數(shù)
/// </summary>
public int MaxConnections
{
get
{
return _maxConnections > 0 ?_maxConnections : 5;
}
set
{
_maxConnections = value;
}
}
private int _socketConnectTimeout;
/// <summary>
/// Socket鏈接超時時間
/// </summary>
public int SocketConnectTimeout
{
get
{
return _socketConnectTimeout > 1000 ? _socketConnectTimeout : 1000;
}
set
{
_socketConnectTimeout = value;
}
}
private int _socketTimeout;
/// <summary>
/// socket超時時間
/// </summary>
public int SocketTimeout
{
get
{
return _socketTimeout > 1000 ? _maintenanceSleep : 3000;
}
set
{
_socketTimeout = value;
}
}
private int _maintenanceSleep;
/// <summary>
/// 維護線程休息時間
/// </summary>
public int MaintenanceSleep
{
get
{
return _maintenanceSleep > 0 ? _maintenanceSleep : 30;
}
set
{
_maintenanceSleep = value;
}
}
private bool _failOver;
/// <summary>
/// 鏈接失敗后是否重啟,詳情參見http://baike.baidu.com/view/1084309.htm
/// </summary>
public bool FailOver
{
get
{
return _failOver;
}
set
{
_failOver = value;
}
}
private bool _nagle;
/// <summary>
/// 是否用nagle算法啟動socket
/// </summary>
public bool Nagle
{
get
{
return _nagle;
}
set
{
_nagle = value;
}
}
}
這些參數(shù)我們通過注釋應該有一些了解,可以說memcached的主要性能都是通過這些參數(shù)來決定的,大家
應根據(jù)自己公司產品和應用的實際情況配置相應的數(shù)值。
當然,做完這一步之后就是對調用“緩存策略”的主體類進行修改來,使其根據(jù)對管理后臺的設計來決定
加載什么樣的緩存策略,如下:
/// <summary>
/// Discuz!NT緩存類
/// 對Discuz!NT論壇緩存進行全局控制管理
/// </summary>
public class DNTCache
{

.
//通過該變量決定是否啟用MemCached
private static bool applyMemCached = MemCachedConfigs.GetConfig().ApplyMemCached;
/// <summary>
/// 構造函數(shù)
/// </summary>
private DNTCache()
{
if (applyMemCached)
cs = new MemCachedStrategy();
else
{
cs = new DefaultCacheStrategy();
objectXmlMap = rootXml.CreateElement("Cache");
//建立內部XML文檔.
rootXml.AppendChild(objectXmlMap);
//LogVisitor clv = new CacheLogVisitor();
//cs.Accept(clv);
cacheConfigTimer.AutoReset = true;
cacheConfigTimer.Enabled = true;
cacheConfigTimer.Elapsed += new System.Timers.ElapsedEventHandler(Timer_Elapsed);
cacheConfigTimer.Start();
}
}


到這里,主要的開發(fā)和修改基本上就告一段落了。下面開始介紹一下如果使用Stats命令來查看緩存的
分配和使用等情況。之前在枚舉類型Stats中看到該命令有幾個主要的參數(shù),分別是:
stats
stats reset
stats malloc
stats maps
stats sizes
stats slabs
stats items
stats cachedump slab_id limit_num
stats detail [on|off|dump]
而JAVAEYE的 robbin 寫過一篇文章:
貼一段遍歷memcached緩存對象的小腳本,來介紹如何使用其中的
“stats cachedump”來獲取信息。受這篇文章的啟發(fā),我將MemCachedClient.cs文件中的Stats方法加以修
改,添加了一個command參數(shù)(字符串型),這樣就可以向緩存服務器發(fā)送上面所說的那幾種類型的命令了。
測試代碼如下:
protected void Submit_Click(object sender, EventArgs e)
{
ArrayList arrayList = new ArrayList();
arrayList.Add("10.0.1.52:11211");//緩存服務器的地址
StateResult.DataSource = MemCachedManager.GetStats(arrayList, (MemCachedManager.Stats)
Utils.StrToInt(StatsParam.SelectedValue, 0), Param.Text);
StateResult.DataBind();
}
頁面代碼如下:

我這樣做的目的有兩個,一個是避免每次都使用telnet協(xié)議遠程登陸緩存服務器并輸入相應的命令行
參數(shù)(我記憶力不好,參數(shù)多了之后就愛忘)。二是將來會把這個頁面功能內置到管理后臺上,以便后臺
管理員可以動態(tài)監(jiān)測每臺緩存服務器上的數(shù)據(jù)。
好了,到這里今天的內容就差不多了。在本文中我們看到了使用設計模式的好處,通過它我們可以讓
自己寫的代碼支持“變化”。這里不妨再多說幾句,大家看到了velocity在使用上也是很方便,如果可以
的話,未來可以也會將velocity做成一個“緩存策略”,這樣站長或管理員就可以根據(jù)自己公司的實際情
況來加以靈活配置了。