用Redis做小型Asp.net网站的性能优化
2017-08-28 17:05
507 查看
主题 | 概要 |
---|---|
Redis | Redis作缓存,优化网站性能 |
编辑 | 时间 |
新建 | 20170828 |
序号 | 参考资料 |
1 | https://github.com/StackExchange/StackExchange.Redis |
2 | https://yq.aliyun.com/ziliao/89080 |
3 | https://www.quartz-scheduler.net/ |
解决问题
当网站中有发布公告、发布新闻的场景时,可能会有发布人员的选择模块。而就是这个模块,弹出整个公司的组织结构和人员,会很耗时,大概要几十秒钟。现在的方案是请求一次,就去库里面查信息,并组建成树型结构。每个人就算获取的是同样的数据,都要操作一遍,如果并发人数多,会加重服务器压力,影响体验。
解决方案
考虑引进缓存,现在的方案是部署一个Redis库,Redis库只做缓存,不做持久化。Redis库每晚凌晨进行一次全量更新,同步mysql里面的人员和部门数据。白天每隔2个小时进行增量更新;如果人数有变化,则更新人员,如果部门有变化,则进行全量更新;如果都没变化,则不做更新。同时提供了一个手动更新的页面,如果确切知道了信息有改变,就通过手动进行更新。应用程序直接从Redis里面取数据,并存到内存中。数据格式为部门数组,每个部门下面挂接部门人员和子部门,子部门下面又挂机人员和子部门的层次结构,是典型的部分-整体设计模式。
整个逻辑如下图:
其中Mysql的表结构不提,就是些关系型表,Redis主要用到了hashSet和sortedSet。
部门数量和人员数量:
组织结构:
需要注意的是组织结构就用了String型的key-value存储,value值就是序列化了部门的层次结构。这个层次结构是构建部门和人员的基石。通过部门ID,从Redis里面获取人员,再查找子部门ID,不断递归。
通过部门ID获取人员,需要给人员建立索引,每个部门下的人员ID组成一个set,key为部门ID。
Web应用从Redis构建结构树的时候,先从组织结构中获取部门ID,通过部门ID获取该部门下所有人员的ID,再通过人员ID获取具体的人员信息。
Redis C#客户端
Redis的安装和配置见后,这里使用Redis官方推荐的StackExchange.Redis(https://github.com/StackExchange/StackExchange.Redis)。建立一个类库项目,从NuGet中安装StackExchange.Redis包。这里参考https://yq.aliyun.com/ziliao/89080对StackExchange.Redis做了简单的封装。
public class RedisConnectionHelp { public static readonly string redisConnStr= System.Web.Configuration.WebConfigurationManager.AppSettings["RedisConnString"]; private static readonly object locker = new object(); //--系统自定义key前缀 public static readonly string SysCustomKeyPrex = "hx:test:"; private static ConnectionMultiplexer instance; /// <summary> /// 单例获取 /// </summary> public static ConnectionMultiplexer Instance { get { if(instance==null) { lock(locker) { if(instance==null || !instance.IsConnected) { instance = GetManager(); } } } return instance; } } public static ConnectionMultiplexer GetManager(string connectionString = null) { var connStr = connectionString ?? redisConnStr; var connect = ConnectionMultiplexer.Connect(connStr); return connect; } }
public class RedisHelper { private int DbNum { get; } private readonly ConnectionMultiplexer conn; /// <summary> /// 自定义Key命名前缀 /// </summary> public string CustomKeyPrex { get; set; } public RedisHelper() { DbNum = 0; conn = RedisConnectionHelp.Instance; } #region 通用辅助类 private string addKeyPrex(string oldStr) { var keyPrex = CustomKeyPrex ?? RedisConnectionHelp.SysCustomKeyPrex; return keyPrex + oldStr; } private T action<T>(Func<IDatabase, T> func) { var database = conn.GetDatabase(DbNum); return func(database); } private string convertJson<T>(T value) { string result = value is string ? value.ToString() : JsonConvert.SerializeObject(value); return result; } public T convertObj<T>(RedisValue value) { if (!value.HasValue) { return default(T); } return JsonConvert.DeserializeObject<T>(value); } private IList<T> convetList<T>(RedisValue[] values) { IList<T> result = new List<T>(); foreach (var item in values) { var model = convertObj<T>(item); result.Add(model); } return result; } private RedisKey[] convertRedisKeys(List<string> redisKeys) { return redisKeys.Select(redisKey => (RedisKey)redisKey).ToArray(); } #endregion #region String /// <summary> /// 保存单个key值 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="expiry">过期时间</param> /// <returns></returns> public bool stringSet(string key, string value, TimeSpan? expiry = default(TimeSpan?)) { key = addKeyPrex(key); return action(db => db.StringSet(key, value, expiry)); } /// <summary> /// 获取单个key的值 /// </summary> /// <param name="key"></param> /// <returns></returns> public string stringGet(string key) { key = addKeyPrex(key); return action(db => db.StringGet(key)); } /// <summary> /// 保存一个对象 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="obj"></param> /// <param name="expiry"></param> /// <returns></returns> public bool stringSet<T>(string key, T obj, TimeSpan? expiry = default(TimeSpan?)) { key = addKeyPrex(key); string json = convertJson<T>(obj); return action(db => db.StringSet(key, json, expiry)); } /// <summary> /// 获取一个key的对象 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public T stringGet<T>(string key) { key = addKeyPrex(key); return action(db => convertObj<T>(db.StringGet(key))); } #endregion #region List /// <summary> /// 移除指定ListId的内部List的值 /// </summary> /// <param name="key"></param> /// <param name="value"></param> public void listRemove<T>(string key, T value) { key = addKeyPrex(key); action(db => db.ListRemove(key, convertJson(value))); } /// <summary> /// 获取指定key的List /// </summary> /// <param name="key"></param> /// <returns></returns> public IList<T> listRange<T>(string key) { key = addKeyPrex(key); return action(redis => { var values = redis.ListRange(key); return convetList<T>(values); }); } /// <summary> /// 入队 /// </summary> /// <param name="key"></param> /// <param name="value"></param> public void listRightPush<T>(string key, T value) { key = addKeyPrex(key); action(db => db.ListRightPush(key, convertJson<T>(value))); } /// <summary> /// 出队 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public T listRightPop<T>(string key) where T : class { key = addKeyPrex(key); return action(db => { var value = db.ListRightPop(key); return convertObj<T>(value); }); } /// <summary> /// 入栈 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="value"></param> public void listLeftPush<T>(string key, T value) { key = addKeyPrex(key); action(db => db.ListLeftPush(key, convertJson(value))); } /// <summary> /// 出栈 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public T listLeftPop<T>(string key) where T : class { key = addKeyPrex(key); return action(db => { var value = db.ListLeftPop(key); return convertObj<T>(value); }); } /// <summary> /// 获取集合中的数量 /// </summary> /// <param name="key"></param> /// <returns></returns> public long listLength(string key) { key = addKeyPrex(key); return action(redis => redis.ListLength(key)); } #endregion #region HASH /// <summary> /// 判断某个数据是否已经被缓存 /// </summary> /// <param name="key"></param> /// <param name="dataKey"></param> /// <returns></returns> public bool hashExists(string key, string dataKey) { key = addKeyPrex(key); return action(db => db.HashExists(key, dataKey)); } /// <summary> /// 存储数据到hash表 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="dataKey"></param> /// <param name="t"></param> /// <returns></returns> public bool hashSet<T>(string key, string dataKey, T t) { key = addKeyPrex(key); return action(db => { string json = convertJson(t); return db.HashSet(key, dataKey, json); }); } /// <summary> /// 移除hash中的某值 /// </summary> /// <param name="key"></param> /// <param name="dataKey"></param> /// <returns></returns> public bool hashDelete(string key, string dataKey) { key = addKeyPrex(key); return action(db => db.HashDelete(key, dataKey)); } /// <summary> /// 移除hash中的多个值 /// </summary> /// <param name="key"></param> /// <param name="dataKeys"></param> /// <returns></returns> public long hashDelete(string key, List<RedisValue> dataKeys) { key = addKeyPrex(key); //List<RedisValue> dataKeys1 = new List<RedisValue>() {"1","2"}; return action(db => db.HashDelete(key, dataKeys.ToArray())); } /// <summary> /// 从hash表获取数据 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="dataKey"></param> /// <returns></returns> public T hashGet<T>(string key, string dataKey) { key = addKeyPrex(key); return action(db => { string value = db.HashGet(key, dataKey); return convertObj<T>(value); }); } /// <summary> /// 为数字增长val /// </summary> /// <param name="key"></param> /// <param name="dataKey"></param> /// <param name="val">可以为负</param> /// <returns>增长后的值</returns> public double hashIncrement(string key, string dataKey, double val = 1) { key = addKeyPrex(key); return action(db => db.HashIncrement(key, dataKey, val)); } /// <summary> /// 为数字减少val /// </summary> /// <param name="key"></param> /// <param name="dataKey"></param> /// <param name="val">可以为负</param> /// <returns>减少后的值</returns> public double hashDecrement(string key, string dataKey, double val = 1) { key = addKeyPrex(key); return action(db => db.HashDecrement(key, dataKey, val)); } /// <summary> /// 获取hashkey所有Redis key /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public IList<T> hashKeys<T>(string key) { key = addKeyPrex(key); return action(db => { RedisValue[] values = db.HashKeys(key); return convetList<T>(values); }); } #endregion #region 有序set /// <summary> /// 添加 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="score"></param> public bool sortedSetAdd<T>(string key, T value, double score) { key = addKeyPrex(key); return action(redis => redis.SortedSetAdd(key, convertJson<T>(value), score)); } /// <summary> /// 删除 /// </summary> /// <param name="key"></param> /// <param name="value"></param> public bool sortedSetRemove<T>(string key, T value) { key = addKeyPrex(key); return action(redis => redis.SortedSetRemove(key, convertJson(value))); } /// <summary> /// 获取全部 /// </summary> /// <param name="key"></param> /// <returns></returns> public IList<T> sortedSetRangeByRank<T>(string key) { key = addKeyPrex(key); return action(redis => { var values = redis.SortedSetRangeByRank(key); return convetList<T>(values); }); } /// <summary> /// 获取集合中的数量 /// </summary> /// <param name="key"></param> /// <returns></returns> public long sortedSetLength(string key) { key = addKeyPrex(key); return action(redis => redis.SortedSetLength(key)); } #endregion #region key管理 /// <summary> /// 删除单个key /// </summary> /// <param name="key">redis key</param> /// <returns>是否删除成功</returns> public bool keyDelete(string key) { key = addKeyPrex(key); return action(db => db.KeyDelete(key)); } /// <summary> /// 删除多个key /// </summary> /// <param name="keys">rediskey</param> /// <returns>成功删除的个数</returns> public long keyDelete(List<string> keys) { List<string> newKeys = keys.Select(addKeyPrex).ToList(); return action(db => db.KeyDelete(convertRedisKeys(newKeys))); } /// <summary> /// 判断key是否存储 /// </summary> /// <param name="key">redis key</param> /// <returns></returns> public bool keyExists(string key) { key = addKeyPrex(key); return action(db => db.KeyExists(key)); } /// <summary> /// 重新命名key /// </summary> /// <param name="key">就的redis key</param> /// <param name="newKey">新的redis key</param> /// <returns></returns> public bool keyRename(string key, string newKey) { key = addKeyPrex(key); return action(db => db.KeyRename(key, newKey)); } /// <summary> /// 设置Key的时间 /// </summary> /// <param name="key">redis key</param> /// <param name="expiry"></param> /// <returns></returns> public bool keyExpire(string key, TimeSpan? expiry = default(TimeSpan?)) { key = addKeyPrex(key); return action(db => db.KeyExpire(key, expiry)); } #endregion key #region 其他 public ITransaction createTransaction() { return getDatabase().CreateTransaction(); } public IDatabase getDatabase() { return conn.GetDatabase(DbNum); } public IServer getServer(string hostAndPort) { return conn.GetServer(hostAndPort); } #endregion #region Batch批量处理 public IBatch createBatch() { return getDatabase().CreateBatch(); } public Task<bool> batchHashSetAsync<T>(IBatch batch, string key, string dataKey, T t) { key = addKeyPrex(key); var json = convertJson(t); return batch.HashSetAsync(key, dataKey, json); } public Task<bool> batchSortedSetAdd<T>(IBatch batch, string key, T value, double score) { key = addKeyPrex(key); var json = convertJson(value); return batch.SortedSetAddAsync(key, json, score); } public void batchExecute(IBatch batch) { batch.Execute(); } public Task<RedisValue> batchHashGetAsync(IBatch batch, string key, string dataKey) { key = addKeyPrex(key); return batch.HashGetAsync(key, dataKey); } public Task<RedisValue[]> batchSortedSetRangeByRankAsync(IBatch batch,string key) { key = addKeyPrex(key); return batch.SortedSetRangeByRankAsync(key); } #endregion }
这里只是摘取了需要的部分,并封装了批量读取的功能。
部分-整体模式
类似公司的组织结构是典型的部分-整体模式。抽像类,定义一个增加组件的方法:
具体类中实现该方法:
从Redis中获取数据,并递归构造成一个单例模式的树型结构:
private void buildOrg(DepartmentInfo[] dept,ConstructOrgComponent orgRoot) { foreach(var d in dept) { redis.CustomKeyPrex = InitPrexConstant.DepEmployeeInID; //--取得该部门下的人员ID var idList=redis.sortedSetRangeByRank<string>(d.ID); List<Task<RedisValue>> taskBaseInfos = new List<Task<RedisValue>>(); List<Task<RedisValue>> taskSimpInfos = new List<Task<RedisValue>>(); var batch = redis.createBatch(); foreach (var id in idList) { redis.CustomKeyPrex = InitPrexConstant.EmployeeBase; //var eBase = redis.hashGet<EmployeeBaseInfo>(id, "EmployeeBase"); var eBase = redis.batchHashGetAsync(batch,id, "EmployeeBase"); taskBaseInfos.Add(eBase); redis.CustomKeyPrex = InitPrexConstant.EmployeeSimple; //var eSimple = redis.hashGet<EmployeeSimpleInfo>(id, "EmployeeSimple"); var eSimple = redis.batchHashGetAsync(batch,id, "EmployeeSimple"); taskSimpInfos.Add(eSimple); } redis.batchExecute(batch); List<EmployeeBaseInfo> baseInfos = new List<EmployeeBaseInfo>(); List<EmployeeSimpleInfo> simpInfos = new List<EmployeeSimpleInfo>(); for (var i=0;i<taskBaseInfos.Count;i++) { var baseTemp = JsonConvert.DeserializeObject<EmployeeBaseInfo>(taskBaseInfos[i].Result.ToString()); baseInfos.Add(baseTemp); var simpTemp = JsonConvert.DeserializeObject<EmployeeSimpleInfo>(taskSimpInfos[i].Result.ToString()); simpInfos.Add(simpTemp); } ConstructOrgComposite orgTemp = new ConstructOrgComposite(d,baseInfos,simpInfos); orgRoot.addOrgComponent(orgTemp); buildOrg(d.Children,orgTemp); } }
由于展示的不是所有的代码,只是把这个过程表述下。传入的参数dept部门数组是树形结构,含有子部门。orgRoot是迭代构造的组织机构。
调用的时候,先虚拟一个根,通过遍历部门层次,不断递归。
private void build() { try { redis.CustomKeyPrex = InitPrexConstant.OrgPre; var orgStr = redis.stringGet(""); OrganizationInfo orgInfo = redis.convertObj<OrganizationInfo>(orgStr); DepartmentInfo rootDept = new DepartmentInfo(); //--虚拟根部门 rootDept.Children = orgInfo.Children[0].Departments; orgComposite = new ConstructOrgComposite(rootDept, new List<EmployeeBaseInfo>(), new List<EmployeeSimpleInfo>()); buildOrg(rootDept.Children, orgComposite); } catch (Exception ex) { HX.Common.Platform.Log(LogLevel.Error, ex.Message); } }
注意从redis获取人员的时候,按部门进行批读取,而不是每次读取一个人员。
最后三句代码:
根据部门、人员信息构造当前组织,并添加到父组织中。接着,当前组织又作为父组织不断递归。
策略模式与Quartz
策略模式是为了消除if块引进的,这个模式比较简单。定义一个策略基类:
一个具体的增量策略类InitStrategyAddUpdate和全量策略类,继承自策略基类并实现doInit方法。
一个初始化类作为上下文,维护一个对具体策略的引用。
public class RedisInit { private InitStrategyBase initStrategy; public RedisInit(InitStrategyBase initStrategy) { this.initStrategy = initStrategy; } public void doInit() { initStrategy.doInit(); } }
一个初始化类作为上下文,维护一个对具体策略的引用,典型的多态。
为了管理策略的调度,引用Quartz程序集(https://www.quartz-scheduler.net/),具体的使用和触发器规则可以参见官网。
这里分别为两种更新方式定义两个job。
增量job:
class AddUpdateJob : IJob { public void Execute(IJobExecutionContext context) { RedisInit redisInit = new RedisInit(new InitStrategyAddUpdate() { OnInitSuccess = ConstructOrgBuild.Instance.resetInsance }); redisInit.doInit(); } }
全量job:
// <summary> /// 全量更新任务,每天凌晨2点 /// </summary> public class AllUpdateJob : IJob { public void Execute(IJobExecutionContext context) { RedisInit redisInit = new RedisInit(new InitStrategyAllUpdate() { OnInitSuccess = ConstructOrgBuild.Instance.resetInsance }); redisInit.doInit(); } }
每个Job绑定不同的策略。
定义一个封装类,Schedule这两个Job。
public class UpdateRedisJobWorker { private static IScheduler scheduler= StdSchedulerFactory.GetDefaultScheduler(); public static void startUpdateJob() { scheduler.Start(); IJobDetail allUpdateJob = JobBuilder.Create<AllUpdateJob>() .WithIdentity("allUpdate", "Redis") .Build(); var nightStrategy= System.Web.Configuration.WebConfigurationManager.AppSettings["RedisUpdateStrategyNight"]; ITrigger allUpdateTrigger = TriggerBuilder.Create() .WithIdentity("allUpdateTrigger", "RedisTrigger") .ForJob(allUpdateJob) .WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(int.Parse(nightStrategy), 00)) // 每天2:00执行一次 .Build(); scheduler.ScheduleJob(allUpdateJob, allUpdateTrigger); IJobDetail addUpdateJob = JobBuilder.Create<AddUpdateJob>() .WithIdentity("addUpdate", "Redis") .Build(); var dayStrategy = System.Web.Configuration.WebConfigurationManager.AppSettings["RedisUpdateStrategyDay"]; ITrigger addUpdateTrigger = TriggerBuilder.Create() .WithIdentity("addUpdateTrigger", "RedisTrigger") .ForJob(addUpdateJob) .WithCronSchedule(dayStrategy) // 每天8~18点,每隔两小时执行一次 .Build(); scheduler.ScheduleJob(addUpdateJob, addUpdateTrigger); } public static void shutdownUpdateJob() { scheduler.Shutdown(); } }
并在Web应用的Application_Start中调用这个方法。
更新策略放到web配置文件中,下面第一行表示8-18点,每隔两小时进行一次增量更新。
Redis安装与配置
安装
从https://github.com/MicrosoftArchive/redis/releases选择最新安装包下载并安装。安装好后,把它启动成windows服务。
运行services.msc命令:
启动redis,默认安装完后已经启动。
配置
配置安装目录下的redis.windows-service.conf文件。注释掉Save,不进行持久化
stop-writes-on-bgsave-error置为no:
重置密码:
Redis默认只能本机访问,如果需要远程连接,则需要:
注释掉bind:
保护模式设为no:
注意:服务器上.net4.0需要安装更新包NDP40-KB2468871-v2-x64,否则会报无法加载程序集的错误,导致连不上redis。
相关文章推荐
- ASP.NET网站的性能优化
- 26种提高ASP.NET网站访问性能的优化方法
- 不修改代码优化 ASP.NET 网站性能的一些方法
- 不修改代码就能优化ASP.NET网站性能的一些方法
- 26种提高ASP.NET网站访问性能的优化方法
- ASP.NET网站性能优化 需要考虑哪些方面
- 不修改代码优化ASP.NET网站性能的一些方法
- Asp.Net 网站性能优化 缓字决 (上) 缓冲写数据
- Asp.Net 网站性能优化 缓字决 (上) 缓冲写数据
- Asp.Net 网站性能优化之缓字决 (上) 缓冲写数据
- 不修改代码就能优化ASP.NET网站性能的一些方法
- Asp.net 网站性能优化二则分享
- Asp.net 网站性能优化二则分享
- asp.net小谈网站性能优化
- Asp.Net 网站性能优化 缓字决 (上) 缓冲写数据
- 26种提高ASP.NET网站访问性能的优化方法
- Asp.Net 网站性能优化之缓字决 (上) 缓冲写数据
- Asp.Net 网站性能优化 缓字决 Asp.Net 内置的输出缓存介绍
- asp.net网站性能优化2则
- [翻译]ASP.NET 2.0网站应用程序的性能优化