一、概述
由于架构设计一里面如果多平台公用相同Key的缓存更改配置后需要多平台上传最新的缓存配置文件来更新,比较麻烦,更新了架构设计二实现了缓存配置的集中管理,不过这样有有了过于中心化的问题,后续在看看如何修改
总体设计思路如下:
项目结构如下:
二、服务端(提供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); } } }
三、客户端(根据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两个对外开放的操作类
使用步骤:
1、向KeyNames.cs中添加一个唯一的KeyName用于找到指定的Key配置;
2、向服务端的KeyConfigList.xml中添加此KeyName的缓存配置(此功能的提供方式可以自己定义,如直接修改文件或提供管理程序等);
3、使用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(); } }
如有什么可以改进的地方还请不吝指点,谢谢