用Redis做小型Asp.net網站的效能優化
主題 | 概要 |
---|---|
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 2.0網站應用程式的效能優化ASP.NET網站優化
- 網站效能優化網站優化
- 中小型網站架構分析及優化網站架構優化
- 做SEO優化網站排名技巧優化網站
- Redis 效能優化Redis優化
- 啟用 Redis 快取優化您的網站訪問速度Redis快取優化網站
- 淺談網站效能之前端效能優化網站前端優化
- 網站效能優化實戰(二)網站優化
- 不修改程式碼優化ASP.NET網站效能的一些方法優化ASP.NET網站
- 網站效能優化的三重境界網站優化
- 關於網站效能優化準則網站優化
- 雅虎網站效能優化的34條軍規!網站優化
- 【tronic】Asp.Net效能優化.ASP.NET優化
- Web 效能優化:21 種優化 CSS 和加快網站速度的方法Web優化CSS網站
- IIS網站伺服器效能優化攻略網站伺服器優化
- 大型網站--前端效能優化和規範網站前端優化
- 網站SEO優化推廣要怎麼做?網站優化
- 【譯】Web 效能優化:21種優化CSS和加快網站速度的方法Web優化CSS網站
- 提高網站訪問效能之Tomcat優化網站Tomcat優化
- [譯] 提高 10 倍效能:優化靜態網站優化網站
- 網站優化網站優化
- [譯] 正在消失的小型網站網站
- 遊戲網站如何做SEO優化推廣呢?遊戲網站優化
- ASP.NET中常用的優化效能的方法ASP.NET優化
- [譯]Web 效能優化: 圖片優化讓網站大小減少 62%Web優化網站
- 優化SOLIDWORKS的工作站效能優化Solid
- ASP.NET十個有效效能優化的方法ASP.NET優化
- 網站效能優化從入門到粗通(PHP 篇)網站優化PHP
- web效能優化系列之網站瓶頸識別Web優化網站
- 【THICK】ASP.NET中常用的優化效能的方法ASP.NET優化
- 網頁效能優化網頁優化
- 優化Angular應用的效能優化Angular
- 網站如何優化網站優化
- 網站SEO優化網站優化
- WordPress網站優化網站優化
- 【面經】面試官:做過效能優化的工作嗎?你會從哪些方面入手做效能優化呢?面試優化
- 網站效能最佳化網站
- 中小型網站架構分析及最佳化網站架構