1、概述node
因爲架構設計一里面若是多平臺公用相同Key的緩存更改配置後須要多平臺上傳最新的緩存配置文件來更新,比較麻煩,更新了架構設計二實現了緩存配置的集中管理,不過這樣有有了過於中心化的問題,後續在看看如何修改redis
整體設計思路以下:數據庫
項目結構以下:數組
2、服務端(提供Key配置文件管理及將Key配置存到緩存中)緩存
KeyConfigList.xml緩存Key配置文件【與前一版一致】安全
<?xml version="1.0" encoding="utf-8" ?> <configuration> <!-- name:程序key、key:緩存key(用:冒號來對緩存數據進行分級)、 validTime:有效時間(單位:分)、enabled:是否有效、{0}:佔位符替換相關id等標識 --> <list> <!-- 一個佔位符key --> <item name="Admin_User_Session" key="Admin:User:Session:{0}" validTime="60" enabled="true"></item> <!-- 無佔位符key --> <item name="Admin_User_List" key="Admin:User:List" validTime="30" enabled="true"></item> <!-- 多個佔位符key --> <item name="Admin_User_Search" key="Admin:User:Search:{0}:{1}:{2}" validTime="5" enabled="true"></item> </list> </configuration>
KeyEntity.cs是對應xml文件的數據實體類(與客戶端通用)服務器
/// <summary> /// Key配置對象(公開) /// 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; } }
RedisManager.cs是Redis的管理類使用的StackExchange.Redis.dll(與客戶端通用)架構
/// <summary> /// Redis緩存管理類 /// Author:taiyonghai /// CreateTime:2017-08-28 /// </summary> public static class RedisManager { //Redis鏈接對象 private static IConnectionMultiplexer redisMultiplexer; //程序鎖 private static object objLock = new object(); //Redis鏈接串(多個服務器用逗號隔開)"10.11.12.237:6379, password='',keepalive=300,connecttimeout=5000,synctimeout=1000" private static readonly string connectStr = "10.11.12.237:6379"; /// <summary> /// 靜態構造用於註冊監聽事件 /// </summary> static RedisManager() { //註冊事件 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> /// 內部錯誤 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void InternalError(object sender, InternalErrorEventArgs e) { //e.Exception } /// <summary> /// 發生錯誤 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private static void ErrorMessage(object sender, RedisErrorEventArgs e) { //e.Message } /// <summary> /// 得到鏈接對象 /// </summary> /// <returns></returns> private static IConnectionMultiplexer GetMultiplexer() { if (redisMultiplexer == null || !redisMultiplexer.IsConnected) { lock (objLock) { //建立Redis鏈接對象 redisMultiplexer = ConnectionMultiplexer.Connect(connectStr); } } return redisMultiplexer; } /// <summary> /// 得到客戶端對象 /// </summary> /// <param name="db">選填指明使用那個數據庫0-16</param> /// <returns></returns> public static IDatabase GetClient(int db = -1) { return GetMultiplexer().GetDatabase(db); } }
KeyServer.cs是提供緩存配置文件管理、監聽、存儲等功能的核心類併發
/// <summary> /// 緩存Key管理 /// Author:taiyonghai /// CreateTime:2017-08-28 /// </summary> public static class KeyServer { //緩存MQ監聽的Key[須要與客戶端相同] private static string mqConfigKey = "MQConfigKey"; //緩存Key配置的Key[須要與客戶端相同] private static string cacheConfigKey = "CacheConfigKey"; //KeyName集合 private static List<HashEntry> keyConfigList; //鎖對象 private static object objLock = new object(); //監控文件對象 private static FileSystemWatcher watcher; //緩存Key配置文件路徑 private static readonly string configFilePath = AppDomain.CurrentDomain.BaseDirectory.Replace("\\bin\\Debug", String.Empty) + "Server\\Configs\\"; //緩存Key配置文件名 private static readonly string configFileName = "KeyConfigList.xml"; /// <summary> /// 靜態構造只執行一次 /// </summary> static KeyServer() { try { //建立對配置文件夾的監聽,若是遇到文件更改則清空KeyNameList,從新讀取 watcher = new FileSystemWatcher(); watcher.Path = configFilePath;//監聽路徑 watcher.Filter = configFileName;//監聽文件名 watcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.LastWrite | NotifyFilters.Size;//僅監聽文件建立時間、文件變動時間、文件大小 watcher.Changed += new FileSystemEventHandler(OnChanged); watcher.EnableRaisingEvents = true;//最後開啓監聽 } catch (Exception e) { throw new ApplicationException("建立監聽程序錯誤,檢查監聽路徑或文件是否存在"); } //初始執行一次讀取文件並存入緩存 ReaderKeyFile(); } /// <summary> /// 讀取KeyName文件 /// </summary> private static void ReaderKeyFile() { if (keyConfigList == null || keyConfigList.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); //一次讀完整個文檔 XmlDocument xmlDoc = new XmlDocument(); xmlDoc.Load(xmlReader); xmlReader.Close();//關閉讀取對象 //獲取指定節點下的全部子節點 XmlNodeList nodeList = xmlDoc.SelectSingleNode("//configuration//list").ChildNodes; //得到一個線程安全的Hashtable對象 keyConfigList = new List<HashEntry>(); //將xml中的屬性賦值給Hashtable foreach (XmlNode node in nodeList) { XmlElement element = (XmlElement)node;//轉爲元素獲取屬性 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")); keyConfigList.Add(new HashEntry(entity.Name, JsonSerializer.SerializeToString(entity))); } //存入緩存 RedisManager.GetClient().HashSet(cacheConfigKey, keyConfigList.ToArray()); } } } /// <summary> /// 變動事件會觸發兩次是正常狀況,是系統保存文件機制致使 /// </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()) { keyConfigList = null; RedisManager.GetClient().KeyDelete(cacheConfigKey); ReaderKeyFile(); //由於此事件會被調用兩次,因此裏面的代碼要有幕等性 } } } /// <summary> /// 重建緩存配置 /// </summary> private static void RebuildCacheConfig() { //本地靜態對象有值則直接存入Cache,無值則從新讀取物理文件 if (keyConfigList == null || keyConfigList.Count == 0) ReaderKeyFile(); else RedisManager.GetClient().HashSet(cacheConfigKey, keyConfigList.ToArray()); } /// <summary> /// 監視緩存配置消息 /// </summary> public static void MonitorCacheConfigMsg() { //讀取隊列是否有消息(採用有序集合的方式代替隊列避免併發請求時重複消息過多去重的操做) //var value = RedisManager.GetClient().ListLeftPop(mqConfigKey); var value = RedisManager.GetClient().SortedSetRangeByRank(mqConfigKey); if (value.Length > 0) { //檢查緩存是否存在配置信息 var hash = RedisManager.GetClient().HashGetAll(cacheConfigKey); if (hash == null || hash.Length == 0) { //不存在則重建配置 RebuildCacheConfig(); } //重建後刪除有序集合中的消息 RedisManager.GetClient().SortedSetRemove(mqConfigKey, 0); } } }
服務端會讀取消息來監控緩存是否含有配置信息因此服務端須要掛在成一個WindowsService服務異步
class Program { static void Main(string[] args) { while (true) { try { KeyServer.MonitorCacheConfigMsg(); } catch (Exception e) { Console.WriteLine(e.Message); } System.Threading.Thread.Sleep(1000); } } }
3、客戶端(根據KeyNames獲取緩存中的配置,而後根據配置讀取緩存數據或建立緩存數據,若是沒有配置則會返回NULL併發送消息申請建立緩存配置,主業務不會中斷會直接穿透到數據庫取值)
KeyEntity.cs 和 RedisManager.cs的代碼和服務端的一致,有須要拷貝便可!
CacheProvider.cs是對外提供緩存功能的類【與前一版一致】
/// <summary> /// 緩存提供類 /// Author:taiyonghai /// CreateTime:2017-08-28 /// </summary> public class CacheProvider { #region Cache 刪除 /// <summary> /// 刪除緩存 /// </summary> /// <param name="name">Key名稱</param> /// <returns>True成功,Flase失敗</returns> public static bool DelKey(KeyNames name) { return DelKey(name, null); } /// <summary> /// 刪除緩存[核心] /// </summary> /// <param name="name">Key名稱</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> /// <returns>True成功,Flase失敗</returns> public static bool DelKey(KeyNames name, params string[] identities) { var entity = KeyClient.Get(name, identities); if (null == entity) return false; else return RedisManager.GetClient().KeyDelete(entity.Key); } #endregion #region Cache String 存取 #region 添加緩存 /// <summary> /// 添加緩存,有過時時間(如Key存在則更新值) /// </summary> /// <param name="name">Key名稱</param> /// <param name="value">不可爲NULL,應對緩存穿透請用空</param> /// <returns>True成功,Flase失敗</returns> public static bool SetString<T>(KeyNames name, T value) where T : class, new() { return SetString<T>(name, value, null); } /// <summary> /// 添加緩存,有過時時間(如Key存在則更新值) /// </summary> /// <param name="name">Key名稱</param> /// <param name="value">不可爲NULL,應對緩存穿透請用空</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> /// <returns>True成功,Flase失敗</returns> public static bool SetString<T>(KeyNames name, T value, params string[] identities) where T : class, new() { //若是value爲null直接緩存無需序列化 string tmpStr = null == value ? null : JsonConvert.SerializeObject(value); return SetString(name, tmpStr, identities); } /// <summary> /// 添加緩存,有過時時間(如Key存在則更新值) /// </summary> /// <param name="name">Key名稱</param> /// <param name="value">不可爲NULL,應對緩存穿透請用空</param> /// <returns>True成功,Flase失敗</returns> public static bool SetString(KeyNames name, string value) { return SetString(name, value, null); } /// <summary> /// 添加緩存,有過時時間[核心](如Key存在則更新值) /// </summary> /// <param name="name">Key名稱</param> /// <param name="value">不可爲NULL,應對緩存穿透請用空</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> /// <returns>True成功,Flase失敗</returns> public static bool SetString(KeyNames name, string value, params string[] identities) { //不可傳值爲NULL,應對緩存穿透請用空,NULL用來判斷是否緩存有值 if (null == value) return false; var entity = KeyClient.Get(name, identities); if (null == entity) return false; else { //有效時間的TimeSpan=(最小時間+有效時間)-最小時間 TimeSpan timeSpan = DateTime.MinValue.AddMinutes(entity.ValidTime) - DateTime.MinValue; return RedisManager.GetClient().StringSet(entity.Key, value, timeSpan); } } #endregion #region 獲取緩存 /// <summary> /// 獲取緩存 /// </summary> /// <param name="name">Key名稱</param> /// <returns>無緩存或禁用緩存均返回NULL</returns> public static T GetString<T>(KeyNames name) where T : class, new() { return GetString<T>(name, null); } /// <summary> /// 獲取緩存 /// </summary> /// <param name="name">Key名稱</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> /// <returns>無緩存或禁用緩存均返回NULL</returns> public static T GetString<T>(KeyNames name, params string[] identities) where T : class, new() { string tmpStr = GetString(name, identities); //若是tmpStr爲null直接返回無需反序列化 return null == tmpStr ? default(T) : JsonConvert.DeserializeObject<T>(tmpStr); } /// <summary> /// 獲取緩存 /// </summary> /// <param name="name">Key名稱</param> /// <returns>無緩存或禁用緩存均返回NULL</returns> public static string GetString(KeyNames name) { return GetString(name, null); } /// <summary> /// 獲取緩存[核心] /// </summary> /// <param name="name">Key名稱</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> /// <returns>無緩存或禁用緩存均返回NULL</returns> public static string GetString(KeyNames name, params string[] identities) { var entity = KeyClient.Get(name, identities); if (null == entity) return null; else { //檢查緩存是否啓用,不然返回NULL if (entity.Enabled) { return RedisManager.GetClient().StringGet(entity.Key); } else { //若是停用緩存,則刪除 DelKey(name); return null; } } } #endregion #endregion #region Cache Hash 存取 #region 添加緩存 /// <summary> /// 添加緩存,無過時時間(如Key存在則更新值) /// </summary> /// <param name="name">Key名稱</param> /// <param name="dict">不可爲NULL,應對緩存穿透請用空對象</param> public static bool SetHash<T>(KeyNames name, Dictionary<string, T> dict) where T : class, new() { return SetHash<T>(name, dict, null); } /// <summary> /// 添加緩存,無過時時間(如Key存在則更新值) /// </summary> /// <param name="name">Key名稱</param> /// <param name="dict">不可爲NULL,應對緩存穿透請用空對象</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> public static bool SetHash<T>(KeyNames name, Dictionary<string, T> dict, params string[] identities) where T : class, new() { var tmpDict = new Dictionary<string, string>(); foreach (var item in dict) tmpDict.Add(item.Key, null == item.Value ? null : JsonConvert.SerializeObject(item.Value)); return SetHash(name, tmpDict, identities); } /// <summary> /// 添加緩存,無過時時間(如Key存在則更新值) /// </summary> /// <param name="name">Key名稱</param> /// <param name="dict">不可爲NULL,應對緩存穿透請用空對象</param> public static bool SetHash(KeyNames name, Dictionary<string, string> dict) { return SetHash(name, dict, null); } /// <summary> /// 添加緩存,無過時時間[核心](如Key存在則更新值) /// </summary> /// <param name="name">Key名稱</param> /// <param name="dict">不可爲NULL,應對緩存穿透請用空對象</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> public static bool SetHash(KeyNames name, Dictionary<string, string> dict, params string[] identities) { //不可傳值爲NULL,應對緩存穿透請用空對象,NULL用來判斷是否緩存有值 if (null == dict) return false; var entity = KeyClient.Get(name, identities); if (null == entity) return false; else { var hashEntryList = new List<HashEntry>(); foreach (var item in dict) hashEntryList.Add(new HashEntry(item.Key, item.Value)); RedisManager.GetClient().HashSet(entity.Key, hashEntryList.ToArray()); return true; } } #endregion #region 獲取緩存 /// <summary> /// 獲取緩存 /// </summary> /// <param name="name">Key名稱</param> /// <returns>無緩存或禁用緩存均返回NULL</returns> public static Dictionary<string, T> GetHash<T>(KeyNames name) where T : class, new() { return GetHash<T>(name, null); } /// <summary> /// 獲取緩存 /// </summary> /// <param name="name">Key名稱</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> /// <returns>無緩存或禁用緩存均返回NULL</returns> public static Dictionary<string, T> GetHash<T>(KeyNames name, params string[] identities) where T : class, new() { var dict = GetHash(name, identities); if (null == dict) return null; else { var tmpDict = new Dictionary<string, T>(); foreach (var item in dict) tmpDict.Add(item.Key, null == item.Value ? default(T) : JsonConvert.DeserializeObject<T>(item.Value)); return tmpDict; } } /// <summary> /// 獲取緩存 /// </summary> /// <param name="name">Key名稱</param> /// <returns>無緩存或禁用緩存均返回NULL</returns> public static Dictionary<string, string> GetHash(KeyNames name) { return GetHash(name, null); } /// <summary> /// 獲取緩存[核心] /// </summary> /// <param name="name">Key名稱</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> /// <returns>無緩存或禁用緩存均返回NULL</returns> public static Dictionary<string, string> GetHash(KeyNames name, params string[] identities) { var entity = KeyClient.Get(name, identities); if (null == entity) return null; else { //檢查緩存是否啓用,不然返回NULL if (entity.Enabled) { var hashEntry = RedisManager.GetClient().HashGetAll(entity.Key); var dict = new Dictionary<string, string>(); foreach (var item in hashEntry) dict.Add(item.Name, item.Value); return dict; } else { //若是停用緩存,則刪除 DelKey(name); return null; } } } #endregion #endregion
MQProvider.cs是對外提供隊列功能的類【與前一版一致】
/// <summary> /// 隊列提供類 /// Author:taiyonghai /// CreateTime:2017-08-31 /// </summary> public class MQProvider { #region MQ 添加 /// <summary> /// 添加一條消息到隊列 /// </summary> /// <param name="name">Key名稱</param> /// <param name="msg">內容</param> /// <returns></returns> public long SetMsg<T>(KeyNames name, T msg) { return SetMsg<T>(name, msg, null); } /// <summary> /// 添加一條消息到隊列 /// </summary> /// <param name="name">Key名稱</param> /// <param name="msg">內容</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> /// <returns></returns> public long SetMsg<T>(KeyNames name, T msg, params string[] identities) { //若是value爲null直接緩存無需序列化 string tmpMsg = null == msg ? null : JsonSerializer.SerializeToString<T>(msg); return SetMsg(name, tmpMsg, identities); } /// <summary> /// 添加一條消息到隊列 /// </summary> /// <param name="name">Key名稱</param> /// <param name="msg">內容</param> /// <returns></returns> public long SetMsg(KeyNames name, string msg) { return SetMsg(name, msg, null); } /// <summary> /// 添加一條消息到隊列[核心] /// </summary> /// <param name="name">Key名稱</param> /// <param name="msg">內容</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> /// <returns>添加消息後的隊列長度</returns> public long SetMsg(KeyNames name, string msg, params string[] identities) { var entity = KeyClient.Get(name, identities); //向隊列右側插入新的消息 return RedisManager.GetClient().ListRightPush(entity.Key, msg); } #endregion #region MQ 獲取 /// <summary> /// 從隊列中獲取一條消息,並將其在隊列中移除 /// </summary> /// <param name="name">Key名稱</param> /// <returns>沒有消息返回NULL</returns> public T GetMsg<T>(KeyNames name) { return GetMsg<T>(name, null); } /// <summary> /// 從隊列中獲取一條消息,並將其在隊列中移除 /// </summary> /// <param name="name">Key名稱</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> /// <returns>沒有消息返回NULL</returns> public T GetMsg<T>(KeyNames name, params string[] identities) { string tmpStr = GetMsg(name, identities); return null == tmpStr ? default(T) : JsonSerializer.DeserializeFromString<T>(tmpStr); } /// <summary> /// 從隊列中獲取一條消息,並將其在隊列中移除 /// </summary> /// <param name="name">Key名稱</param> /// <returns>沒有消息返回NULL</returns> public string GetMsg(KeyNames name) { return GetMsg(name, null); } /// <summary> /// 從隊列中獲取一條消息,並將其在隊列中移除[核心] /// </summary> /// <param name="name">Key名稱</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> /// <returns>沒有消息返回NULL</returns> public string GetMsg(KeyNames name, params string[] identities) { var entity = KeyClient.Get(name, identities); //從隊列左側隊列頭部取出消息 return RedisManager.GetClient().ListLeftPop(entity.Key); } #endregion }
KeyNames.cs配置緩存Key的名稱,用於尋找Key配置【與前一版一致】
/// <summary> /// KeyName枚舉(公開) /// Author:taiyonghai /// CreateTime:2017-08-28 /// </summary> public enum KeyNames { /// <summary> /// 後臺用戶會話key /// </summary> Admin_User_Session, Admin_User_List, Admin_User_Search }
KeyClient.cs是對於使用緩存時尋找Key對應配置信息的核心類
public class KeyClient { //緩存MQ監聽的Key[須要與服務端相同] private static string mqConfigKey = "MQConfigKey"; //緩存Key配置的Key[須要與服務端相同] private static string cacheConfigKey = "CacheConfigKey"; /// <summary> /// 根據KeyName獲取Key配置對象 /// </summary> /// <param name="name">Key名稱</param> /// <returns></returns> public static KeyEntity Get(KeyNames name) { return Get(name, null); } /// <summary> /// 根據KeyName獲取Key配置對象 /// </summary> /// <param name="name">Key名稱</param> /// <param name="identities">Key標識(用於替換Key中的{0}佔位符)</param> /// <returns></returns> public static KeyEntity Get(KeyNames name, params string[] identities) { var value = RedisManager.GetClient().HashGet(cacheConfigKey, name.ToString().ToLower()); if (value.HasValue) { var entity = JsonSerializer.DeserializeFromString<KeyEntity>(value); //檢查Key是否須要含有佔位符 if (entity.Key.IndexOf('{') > 0) { //檢查參數數組是否有值 if (identities != null && identities.Length > 0) entity.Key = String.Format(entity.Key, identities); else throw new ArgumentException("須要此參數identities標識字段,但並未傳遞", "identities"); } return entity; } else { //異步發送消息 SendCreateConfigMsg(); return null; } } public static async Task<bool> SendCreateConfigMsg() { //發送消息重建緩存配置(採用有序集合的方式代替隊列避免併發請求時重複消息過多去重的操做) //RedisManager.GetClient().ListRightPush(mqConfigKey, 0); return await RedisManager.GetClient().SortedSetAddAsync(mqConfigKey, 0, DateTime.Now.Ticks); } }
客戶端使用只會接觸到CacheProvider.cs和MQProvider.cs兩個對外開放的操做類
使用步驟:
一、向KeyNames.cs中添加一個惟一的KeyName用於找到指定的Key配置;
二、向服務端的KeyConfigList.xml中添加此KeyName的緩存配置(此功能的提供方式能夠本身定義,如直接修改文件或提供管理程序等);
三、使用Cache或者MQ的操做類傳遞KeyNames的枚舉值,再傳遞緩存值便可;
public class HomeController : Controller { public ActionResult Index() { CacheProvider.SetString(KeyNames.Admin_User_List, "test"); string str = CacheProvider.GetString(KeyNames.Admin_User_List); return View(); } }
若有什麼能夠改進的地方還請不吝指點,謝謝