一些項(xiàng)目整理出的項(xiàng)目中引入緩存的架構(gòu)設(shè)計(jì)方案磺陡,希望能幫助你更好地管理項(xiàng)目緩存减途,作者水平有限捎废,如有不足還望指點(diǎn)。
一藐鹤、基礎(chǔ)結(jié)構(gòu)介紹
項(xiàng)目中對(duì)外提供方法的是CacheProvider和MQProvider兩個(gè)類瓤檐,一切緩存或隊(duì)列應(yīng)用都從這里做入口,后期更換緩存或隊(duì)列只需要更改后面的提供者即可
主要結(jié)構(gòu)設(shè)計(jì)分為三部分:
1娱节、Key管理(用于管理緩存Key挠蛉、過期時(shí)間、是否啟用肄满、調(diào)用識(shí)別Key等)
Configs -> Cache -> KeyConfigList.xml(配置Key的具體信息)
Cache -> Key -> KeyEntity.cs(XML的序列化對(duì)象)
Cache -> Key -> KeyManager.cs(讀取XML并監(jiān)聽XML文件的變更碌秸,如果變更重新讀壬芤啤)
Cache -> Key -> KeyNames.cs(Key名稱的枚舉悄窃,控制Key從這里集中管理讥电,不會(huì)到處都是)
2、內(nèi)部操作(對(duì)接的多個(gè)緩存實(shí)際提供技術(shù)比如Redis轧抗、Memcached恩敌、LocalCache等)
Cache -> Redis -> RedisManager.cs(Redis的連接對(duì)象及基本配置)
3、對(duì)外提供(對(duì)項(xiàng)目中應(yīng)用緩存提供支持函數(shù)横媚,如更改緩存提供技術(shù)只需從這里調(diào)整代碼纠炮,不影響項(xiàng)目主體代碼)
Cache -> CacheProvider.cs(項(xiàng)目中的緩存操作提供函數(shù)類)
MQ -> MQProvider.cs(項(xiàng)目中的隊(duì)列操作提供函數(shù)類)
二、代碼詳細(xì)介紹
1灯蝴、KeyConfigList.xml
用于存儲(chǔ)緩存中數(shù)據(jù)的Key恢口、有效時(shí)間、是否啟用此緩存等配置信息
name:用來尋找此條Key信息的標(biāo)識(shí)
key:緩存中存的Key
validTime:便于計(jì)算此緩存的有效時(shí)間穷躁,比如只緩存5分鐘
enabled:是否啟用此緩存耕肩,不啟用則每次都讀庫(kù)
{0}、{1}问潭、{2}:緩存Key的占位符用于區(qū)分某個(gè)類型的緩存其中的一個(gè)猿诸,比如商品緩存格式為Goods:{0},可能實(shí)際存儲(chǔ)Key是Goods:1狡忙、Goods:2梳虽、Goods:3,這個(gè)1灾茁、2窜觉、3是商品Id來區(qū)分具體某個(gè),如果量大禁用時(shí)會(huì)導(dǎo)致緩存雪崩北专,可以考慮再根據(jù)類型或其他來細(xì)分
[](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"><?xml version="1.0" encoding="utf-8" ?>
<configuration>
<list>
<item name="Admin_User_Session" key="Admin:User:Session:{0}" validTime="60" enabled="true"></item>
<item name="Admin_User_List" key="Admin:User:List" validTime="30" enabled="true"></item>
<item name="Admin_User_Search" key="Admin:User:Search:{0}:{1}:{2}" validTime="5" enabled="true"></item>
</list>
</configuration></pre>
](javascript:void(0); "復(fù)制代碼")
2禀挫、KeyEntity.cs
這個(gè)比較簡(jiǎn)單,就是把xml的內(nèi)容讀取出來序列化為對(duì)象逗余,只是為了便于檢索特咆,name和key都小寫化了
[](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"> /// <summary>
/// Key配置對(duì)象(公開) /// Author:taiyonghai /// CreateTime:2017-08-28 /// </summary>
public sealed class KeyEntity
{ private string name; /// <summary>
/// Cache Name(Use for search cache key) /// </summary>
public string Name
{ get { return name; } set { name = value.Trim().ToLower(); }
} private string key; /// <summary>
/// Cache Key /// </summary>
public string Key
{ get { return key; } set { key = value.Trim().ToLower(); }
} /// <summary>
/// Valid Time (Unit:minute) /// </summary>
public int ValidTime { get; set; } /// <summary>
/// Enaled /// </summary>
public bool Enabled { get; set; }
}</pre>
](javascript:void(0); "復(fù)制代碼")
3、 KeyManager.cs
負(fù)責(zé)訪問Key配置的XML文件录粱,并將其緩存到靜態(tài)Hashtable中腻格,使用時(shí)直接從中檢索到要用的信息,設(shè)置監(jiān)聽程序FileSystemWatcher如果文件發(fā)生變動(dòng)則重置Hashtable使其重新讀取啥繁,配置文件及名稱可以自行變更或配置
還要提供根據(jù)KeyName獲取Key配置對(duì)象的方法菜职,這樣就可以使用Key存到實(shí)際的緩存中,如果Key需要進(jìn)行構(gòu)造還可以傳送Key的標(biāo)識(shí)數(shù)組旗闽,從此方法中自動(dòng)整合返回
[](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"> /// <summary>
/// 緩存Key管理 /// Author:taiyonghai /// CreateTime:2017-08-28 /// </summary>
public static class KeyManager
{ //KeyName集合
private static Hashtable keyNameList; //鎖對(duì)象
private static object objLock = new object(); //監(jiān)控文件對(duì)象
private static FileSystemWatcher watcher; //緩存Key配置文件路徑
private static readonly string configFilePath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase + "Configs\Cache\"; //緩存Key配置文件名
private static readonly string configFileName = "KeyConfigList.xml"; /// <summary>
/// 靜態(tài)構(gòu)造只執(zhí)行一次 /// </summary>
static KeyManager()
{ //創(chuàng)建對(duì)配置文件夾的監(jiān)聽酬核,如果遇到文件更改則清空KeyNameList蜜另,重新讀取
watcher = new FileSystemWatcher();
watcher.Path = configFilePath;//監(jiān)聽路徑
watcher.Filter = configFileName;//監(jiān)聽文件名
watcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.LastWrite | NotifyFilters.Size;//僅監(jiān)聽文件創(chuàng)建時(shí)間、文件變更時(shí)間嫡意、文件大小
watcher.Changed += new FileSystemEventHandler(OnChanged);
watcher.EnableRaisingEvents = true;//最后開啟監(jiān)聽
} /// <summary>
/// 讀取KeyName文件 /// </summary>
private static void ReaderKeyFile()
{ if (keyNameList == null || keyNameList.Count == 0)
{ //鎖定讀取xml操作
lock (objLock)
{ //獲取配置文件
string configFile = String.Concat(configFilePath, configFileName); //檢查文件
if (!File.Exists(configFile))
{ throw new FileNotFoundException(String.Concat("file not exists:", configFile));
} //讀取xml文件
XmlReaderSettings xmlSetting = new XmlReaderSettings();
xmlSetting.IgnoreComments = true;//忽略注釋
XmlReader xmlReader = XmlReader.Create(configFile, xmlSetting); //一次讀完整個(gè)文檔
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(xmlReader);
xmlReader.Close();//關(guān)閉讀取對(duì)象 //獲取指定節(jié)點(diǎn)下的所有子節(jié)點(diǎn)
XmlNodeList nodeList = xmlDoc.SelectSingleNode("http://configuration//list").ChildNodes; //獲得一個(gè)線程安全的Hashtable對(duì)象
keyNameList = Hashtable.Synchronized(new Hashtable()); //將xml中的屬性賦值給Hashtable
foreach (XmlNode node in nodeList)
{
XmlElement element = (XmlElement)node;//轉(zhuǎn)為元素獲取屬性
KeyEntity entity = new KeyEntity();
entity.Name = element.GetAttribute("name");
entity.Key = element.GetAttribute("key");
entity.ValidTime = Convert.ToInt32(element.GetAttribute("validTime"));
entity.Enabled = Convert.ToBoolean(element.GetAttribute("enabled"));
keyNameList.Add(entity.Name, entity);
}
}
}
} /// <summary>
/// 變更事件會(huì)觸發(fā)兩次是正常情況举瑰,是系統(tǒng)保存文件機(jī)制導(dǎo)致 /// </summary>
/// <param name="source"></param>
/// <param name="e"></param>
private static void OnChanged(object source, FileSystemEventArgs e)
{ if (e.ChangeType == WatcherChangeTypes.Changed)
{ if (e.Name.ToLower() == configFileName.ToLower())
{
keyNameList = null; //因?yàn)榇耸录?huì)被調(diào)用兩次,所以里面的代碼要有幕等性蔬螟,如果無法實(shí)現(xiàn)幕等性此迅, //則應(yīng)該在Init()中綁定事件 //watcher.Changed += new FileSystemEventHandler(OnChanged); //在OnChanged()事件中解綁事件 //watcher.Changed -= new FileSystemEventHandler(OnChanged);
}
}
} /// <summary>
/// 根據(jù)KeyName獲取Key配置對(duì)象 /// </summary>
/// <param name="name">Key名稱</param>
/// <returns></returns>
public static KeyEntity Get(KeyNames name)
{ return Get(name, null);
} /// <summary>
/// 根據(jù)KeyName獲取Key配置對(duì)象 /// </summary>
/// <param name="name">Key名稱</param>
/// <param name="identities">Key標(biāo)識(shí)(用于替換Key中的{0}占位符)</param>
/// <returns></returns>
public static KeyEntity Get(KeyNames name, params string[] identities)
{ //檢查Hash中是否有值
if (keyNameList == null || keyNameList.Count == 0)
KeyManager.ReaderKeyFile(); //檢查Hash中是否有此Key
string tmpName = name.ToString().ToLower(); if (!keyNameList.ContainsKey(tmpName)) throw new ArgumentException("keyNameList中不存在此KeyName", "name"); var entity = keyNameList[tmpName] as KeyEntity; //檢查Key是否需要含有占位符
if (entity.Key.IndexOf('{') > 0)
{ //檢查參數(shù)數(shù)組是否有值
if (identities != null && identities.Length > 0)
entity.Key = String.Format(entity.Key, identities); else
throw new ArgumentException("需要此參數(shù)identities標(biāo)識(shí)字段,但并未傳遞", "identities");
} return entity;
}
}</pre>
[](javascript:void(0); "復(fù)制代碼")
4旧巾、KeyNames.cs
用枚舉類型是為了控制傳遞的KeyName能夠被限制耸序,不會(huì)隨便傳個(gè)string過來導(dǎo)致出錯(cuò),實(shí)際還是使用了KeyNames.Admin_User_Session.ToString()來識(shí)別的鲁猩,此處是根據(jù)枚舉名查找KeyConfigList.xml中的name屬性
[](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"> /// <summary>
/// KeyName枚舉(公開) /// Author:taiyonghai /// CreateTime:2017-08-28 /// </summary>
public enum KeyNames
{ /// <summary>
/// 后臺(tái)用戶會(huì)話key /// </summary>
Admin_User_Session,
Admin_User_List,
Admin_User_Search
}</pre>
[](javascript:void(0); "復(fù)制代碼")
5坎怪、RedisManager.cs
這里可以是Redis也可以是Memcached主要就是提供緩存技術(shù)的管理,熱門的dll有ServiceStack.Redis和StackExchange.Redis廓握,可前者已經(jīng)收費(fèi)(免費(fèi)使用有使用限額)搅窿,無限額免費(fèi)只能用4.0之前的版本,所以采用了后者
IConnectionMultiplexer是核心對(duì)象疾棵,此處使用單例模式創(chuàng)建連接對(duì)象戈钢,因?yàn)閯?chuàng)建連接的資源消耗較高,后面有測(cè)試結(jié)果可以證明
在靜態(tài)構(gòu)造中綁定了幾個(gè)異常事件是尔,如果發(fā)生了錯(cuò)誤可以寫日志便于我們調(diào)試使用殉了,GetDatabase()方法很輕量可以放心直接調(diào)用,配置文件可以采用其他方式
[](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"> /// <summary>
/// Redis緩存管理類 /// Author:taiyonghai /// CreateTime:2017-08-28 /// </summary>
public static class RedisManager
{ //Redis連接對(duì)象
private static IConnectionMultiplexer redisMultiplexer; //程序鎖
private static object objLock = new object(); //Redis連接串(多個(gè)服務(wù)器用逗號(hào)隔開)"10.11.12.237:6379, password='',keepalive=300,connecttimeout=5000,synctimeout=1000"
private static readonly string connectStr = "10.11.12.237:6379"; /// <summary>
/// 靜態(tài)構(gòu)造用于注冊(cè)監(jiān)聽事件 /// </summary>
static RedisManager()
{ //注冊(cè)事件
GetMultiplexer().ConnectionFailed += ConnectionFailed;
GetMultiplexer().InternalError += InternalError;
GetMultiplexer().ErrorMessage += ErrorMessage;
} /// <summary>
/// 連接失敗 /// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void ConnectionFailed(object sender, ConnectionFailedEventArgs e)
{ //e.Exception
} /// <summary>
/// 內(nèi)部錯(cuò)誤 /// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void InternalError(object sender, InternalErrorEventArgs e)
{ //e.Exception
} /// <summary>
/// 發(fā)生錯(cuò)誤 /// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void ErrorMessage(object sender, RedisErrorEventArgs e)
{ //e.Message
} /// <summary>
/// 獲得連接對(duì)象 /// </summary>
/// <returns></returns>
private static IConnectionMultiplexer GetMultiplexer()
{ if (redisMultiplexer == null || !redisMultiplexer.IsConnected)
{ lock (objLock)
{ //創(chuàng)建Redis連接對(duì)象
redisMultiplexer = ConnectionMultiplexer.Connect(connectStr);
}
} return redisMultiplexer;
} /// <summary>
/// 獲得客戶端對(duì)象 /// </summary>
/// <param name="db">選填指明使用那個(gè)數(shù)據(jù)庫(kù)0-16</param>
/// <returns></returns>
public static IDatabase GetClient(int db = -1)
{ return GetMultiplexer().GetDatabase(db);
}
}</pre>
[](javascript:void(0); "復(fù)制代碼")
如果每次都ConnectionMultiplexer.Connect()一個(gè)連接對(duì)象的測(cè)試結(jié)果如下:
采用單例模式處理連接對(duì)象的測(cè)試結(jié)果如下:
6拟枚、CacheProvider.cs
對(duì)項(xiàng)目中提供的緩存操作類薪铜,提供多個(gè)方法,我只提供了String類型和Hash類型恩溅,Set集合類型我用不到就沒有提供隔箍,需要的朋友可以自己添加
View Code
7、MQProvider.cs
對(duì)項(xiàng)目中提供的消息隊(duì)列操作類脚乡,我偷懶應(yīng)用了Redis的List類型來提供消息隊(duì)列的操作蜒滩,少數(shù)據(jù)量的情況下比如msg在10k以下性能很好,大數(shù)據(jù)量時(shí)性能下降嚴(yán)重奶稠,有興趣可以百度一下看看測(cè)試俯艰,但他沒有事務(wù)級(jí)的能力所以小規(guī)模使用可以,需求高還是需要更專業(yè)的隊(duì)列比如RabbitMQ等
View Code
三锌订、項(xiàng)目調(diào)用代碼
Redis如果遇到同樣Key且同類型(String竹握、Hash、List)時(shí)是直接覆蓋值辆飘,如果不同類型的話就會(huì)報(bào)錯(cuò)了啦辐,我偷懶使用了同一個(gè)KeyNames就使用加前綴的方式來區(qū)分同類型不重復(fù)
[](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">CacheProvider cache = new CacheProvider();
MQProvider mq = new MQProvider(); //基礎(chǔ)類型
cache.SetString(KeyNames.Cache_Admin_User_Session, "taiyonghai", "100"); var str = cache.GetString(KeyNames.Cache_Admin_User_Session); //Hash類型
var dict = new Dictionary<string, string>();
dict.Add("1", "待處理");
dict.Add("2", "處理中");
dict.Add("3", "處理完成");
cache.SetHash(KeyNames.Cache_Hash_Admin_User_List, dict); var tmpDict = cache.GetHash(KeyNames.Cache_Hash_Admin_User_List); //List隊(duì)列
mq.SetMsg(KeyNames.Msg_Admin_User_Search, "Hello");
mq.GetMsg(KeyNames.Msg_Admin_User_Search);</pre>
](javascript:void(0); "復(fù)制代碼")