您的位置:首页 > 数据库 > Redis

用Redis做小型Asp.net网站的性能优化

2017-08-28 17:05 507 查看
主题概要
RedisRedis作缓存,优化网站性能
编辑时间
新建20170828
序号参考资料
1https://github.com/StackExchange/StackExchange.Redis
2https://yq.aliyun.com/ziliao/89080
3https://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 redis 优化 性能