C# Redis缓存架构设计一

一些项目整理出的项目中引入缓存的架构设计方案,希望能帮助你更好地管理项目缓存,作者水平有限,如有不足还望指点。

一、基础结构介绍

image

项目中对外提供方法的是CacheProvider和MQProvider两个类,一切缓存或队列应用都从这里做入口,后期更换缓存或队列只需要更改后面的提供者即可

主要结构设计分为三部分:

1、Key管理(用于管理缓存Key、过期时间、是否启用、调用识别Key等)

Configs -> Cache -> KeyConfigList.xml(配置Key的具体信息)

Cache -> Key -> KeyEntity.cs(XML的序列化对象)

Cache -> Key -> KeyManager.cs(读取XML并监听XML文件的变更,如果变更重新读取)

Cache -> Key -> KeyNames.cs(Key名称的枚举,控制Key从这里集中管理,不会到处都是)

2、内部操作(对接的多个缓存实际提供技术比如Redis、Memcached、LocalCache等)

Cache -> Redis -> RedisManager.cs(Redis的连接对象及基本配置)

3、对外提供(对项目中应用缓存提供支持函数,如更改缓存提供技术只需从这里调整代码,不影响项目主体代码)

Cache -> CacheProvider.cs(项目中的缓存操作提供函数类)

MQ -> MQProvider.cs(项目中的队列操作提供函数类)

二、代码详细介绍

1、KeyConfigList.xml

用于存储缓存中数据的Key、有效时间、是否启用此缓存等配置信息

name:用来寻找此条Key信息的标识

key:缓存中存的Key

validTime:便于计算此缓存的有效时间,比如只缓存5分钟

enabled:是否启用此缓存,不启用则每次都读库

{0}、{1}、{2}:缓存Key的占位符用于区分某个类型的缓存其中的一个,比如商品缓存格式为Goods:{0},可能实际存储Key是Goods:1、Goods:2、Goods:3,这个1、2、3是商品Id来区分具体某个,如果量大禁用时会导致缓存雪崩,可以考虑再根据类型或其他来细分

[
复制代码

](javascript:void(0); "复制代码")

<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); "复制代码")

2、KeyEntity.cs

这个比较简单,就是把xml的内容读取出来序列化为对象,只是为了便于检索,name和key都小写化了

[
复制代码

](javascript:void(0); "复制代码")

<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 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); "复制代码")

3、 KeyManager.cs

负责访问Key配置的XML文件,并将其缓存到静态Hashtable中,使用时直接从中检索到要用的信息,设置监听程序FileSystemWatcher如果文件发生变动则重置Hashtable使其重新读取,配置文件及名称可以自行变更或配置

还要提供根据KeyName获取Key配置对象的方法,这样就可以使用Key存到实际的缓存中,如果Key需要进行构造还可以传送Key的标识数组,从此方法中自动整合返回

[
复制代码

](javascript:void(0); "复制代码")

<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; //锁对象
private static object objLock = new object(); //监控文件对象
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>
/// 静态构造只执行一次 /// </summary>
static KeyManager()
{ //创建对配置文件夹的监听,如果遇到文件更改则清空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;//最后开启监听
} /// <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); //一次读完整个文档
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(xmlReader);
xmlReader.Close();//关闭读取对象 //获取指定节点下的所有子节点
XmlNodeList nodeList = xmlDoc.SelectSingleNode("//configuration//list").ChildNodes; //获得一个线程安全的Hashtable对象
keyNameList = Hashtable.Synchronized(new Hashtable()); //将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"));

                    keyNameList.Add(entity.Name, entity);
                }
            }
        }
    } /// <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())
            {
                keyNameList = null; //因为此事件会被调用两次,所以里面的代码要有幕等性,如果无法实现幕等性, //则应该在Init()中绑定事件 //watcher.Changed += new FileSystemEventHandler(OnChanged); //在OnChanged()事件中解绑事件 //watcher.Changed -= new FileSystemEventHandler(OnChanged);

}
}
} /// <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)
{ //检查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)
{ //检查参数数组是否有值
if (identities != null && identities.Length > 0)
entity.Key = String.Format(entity.Key, identities); else
throw new ArgumentException("需要此参数identities标识字段,但并未传递", "identities");
} return entity;
}

}</pre>

[
复制代码

](javascript:void(0); "复制代码")

4、KeyNames.cs

用枚举类型是为了控制传递的KeyName能够被限制,不会随便传个string过来导致出错,实际还是使用了KeyNames.Admin_User_Session.ToString()来识别的,此处是根据枚举名查找KeyConfigList.xml中的name属性

[
复制代码

](javascript:void(0); "复制代码")

<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>
/// 后台用户会话key /// </summary>
Admin_User_Session,

    Admin_User_List,

    Admin_User_Search

}</pre>

[
复制代码

](javascript:void(0); "复制代码")

5、RedisManager.cs

这里可以是Redis也可以是Memcached主要就是提供缓存技术的管理,热门的dll有ServiceStack.Redis和StackExchange.Redis,可前者已经收费(免费使用有使用限额),无限额免费只能用4.0之前的版本,所以采用了后者

IConnectionMultiplexer是核心对象,此处使用单例模式创建连接对象,因为创建连接的资源消耗较高,后面有测试结果可以证明

在静态构造中绑定了几个异常事件,如果发生了错误可以写日志便于我们调试使用,GetDatabase()方法很轻量可以放心直接调用,配置文件可以采用其他方式

[
复制代码

](javascript:void(0); "复制代码")

<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连接对象
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);
}

}</pre>

[
复制代码

](javascript:void(0); "复制代码")

如果每次都ConnectionMultiplexer.Connect()一个连接对象的测试结果如下:

image

采用单例模式处理连接对象的测试结果如下:

image

6、CacheProvider.cs

对项目中提供的缓存操作类,提供多个方法,我只提供了String类型和Hash类型,Set集合类型我用不到就没有提供,需要的朋友可以自己添加

image

View Code

7、MQProvider.cs

对项目中提供的消息队列操作类,我偷懒应用了Redis的List类型来提供消息队列的操作,少数据量的情况下比如msg在10k以下性能很好,大数据量时性能下降严重,有兴趣可以百度一下看看测试,但他没有事务级的能力所以小规模使用可以,需求高还是需要更专业的队列比如RabbitMQ等

image

View Code

三、项目调用代码

Redis如果遇到同样Key且同类型(String、Hash、List)时是直接覆盖值,如果不同类型的话就会报错了,我偷懒使用了同一个KeyNames就使用加前缀的方式来区分同类型不重复

[
复制代码

](javascript:void(0); "复制代码")

<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(); //基础类型
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队列
mq.SetMsg(KeyNames.Msg_Admin_User_Search, "Hello");
mq.GetMsg(KeyNames.Msg_Admin_User_Search);</pre>

[
复制代码

](javascript:void(0); "复制代码")

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,600评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,587评论 18 399
  • 一. Java基础部分.................................................
    wy_sure阅读 3,793评论 0 11
  • 人类是生来就具有损毁欲的生物。 幼儿会涂抹白墙,踩踏草坪,拿着折来的树枝到处挥舞。 越是无瑕洁净的东西就越是不能放...
    洌灵阅读 224评论 0 2
  • 有点财产就要脸了,有些话张不开口有些事儿下不了手,最后终于进化为好面子的伪中产阶级,毕竟不好意思自己跟自己承认,钱...
    茶小狗子阅读 121评论 0 0