如果我們網站的某些頁面流量比較大,而且會進行較大量的資料庫操作,或是使用CPU來做高度的運算,但可以接受使用者不一定需要看到最新的資料,並允許短暫時間的誤差 (例如: 五分鐘),這時候我們就可以考慮在這個頁面上加上Cache來減低Server的負擔。最常應用Cache的場景應該就是網站的首頁了,網站首頁通常會包含大量的資訊,也擁有最大量的瀏覽量,但可以容許資料十分鐘才更新一次,正是適合Cache的使用。今天我們也將舉一個簡單的範例來實作Cache,並逐步的調整使用的方法!
在網站系統中,我們經常會使用Cache來加快網站的回應速度,而如何讓Cache使用的效益最佳化也是一個很值得討論的議題,今天的文章主要是實作一些抽象的想法,並不一定可以直接套用在正式的網站之中,也歡迎大家一起分享自己的意見,未來也會撰寫一些類似系列的文章,來實作一些有趣的想法。
如果我們網站的某些頁面流量比較大,而且會進行較大量的資料庫操作,或是使用CPU來做高度的運算,但可以接受使用者不一定需要看到最新的資料,並允許短暫時間的誤差 (例如: 五分鐘),這時候我們就可以考慮在這個頁面上加上Cache來減低Server的負擔。最常應用Cache的場景應該就是網站的首頁了,網站首頁通常會包含大量的資訊,也擁有最大量的瀏覽量,但可以容許資料十分鐘才更新一次,正是適合Cache的使用。今天我們也將舉一個簡單的範例來實作Cache,並逐步的調整使用的方法!
模擬一個需要較長時間回應的頁面
首先我們實作一個頁面來呈現一個清單,但為了模擬長時間的運算,所以撰寫Service如下,在執行時會Delay5秒鐘才回傳資料
public interface ILongRunningService
{
CacheDto> GetData(int min, int max);
}
public class LongRunningService : ILongRunningService
{
public CacheDto> GetData(int min, int max)
{
Thread.Sleep(5000);
var r = new Random();
var result = new List();
for (int i = 0; i < 10; i++)
{
result.Add(r.Next(min, max));
}
return new CacheDto>
{
Data = result,
UpdateTime = DateTime.Now
};
}
}
/int> /ienumerable
CacheDto主要作為資料交換使用,並可以顯示資料被更新的時間
public class CacheDto
{
public T Data { get; set; }
public DateTime UpdateTime { get; set; }
}
public class CacheDto: CacheDto
將內容呈現在網頁上
你可以從Github tag: Basic website取得網站雛形
參考資料:
使用Redis做為網站的快取服務
Redis為一個KeyValue Pair類型的資料庫,也經常被使用作為Cache的場景,現在Azure之中也有提供Azure Redis Cache的服務,雖然目前是Preview版本,但我想應該不需要太久正式版就會出現在大家面前,本篇的範例也將使用Redis作為Cache的Server來儲存資料,接下來就向大家介紹如何將資料存放在Redis之中
-
安裝Redis服務,可以直接在Azure上申請,或是使用nuget下載
Install-Package Redis-64
使用Nuget下載後,直接執行redis-server.exe即可
-
安裝Redis Client,我們使用StackExchange.Redis作為Redis Client
Install-Package StackExchange.Redis
-
在Autofac中註冊Redis的連線,並設定為Singleton (官方文件建議保留Connection,重複使用)
builder.Register(i => { var connect = ConnectionMultiplexer.Connect("localhost"); return connect; }).AsSelf() .SingleInstance();
-
新增一個StackExchangeRedisExtension的Class,擴充IDatabase介面,為設定和取得提供泛型及自動序列化的方法 (參考自MSDN)
public static class StackExchangeRedisExtension { public static T Get
(this IDatabase cache, string key) { return Deserialize (cache.StringGet(key)); } public static object Get(this IDatabase cache, string key) { return Deserialize -
改寫LongRunningService,改為透過Cache取得資料,若沒有資料才直接產生,並寫入Cache中
public class LongRunningService : ILongRunningService { public ConnectionMultiplexer RedisConnection { get; set; } public LongRunningService(ConnectionMultiplexer connection) { this.RedisConnection = connection; } public CacheDto
> GetData(int min, int max) { var cache = this.RedisConnection.GetDatabase(); var key = string.Format("LongRunningService.GetData.{0}.{1}", min, max); var cachedData = cache.Get >>(key); if (cachedData == null) { Thread.Sleep(5000); var r = new Random(); var result = new List (); for (int i = 0; i < 10; i++) { result.Add(r.Next(min, max)); } cachedData = new CacheDto > { Data = result, UpdateTime = DateTime.Now }; cache.Set(key, cachedData, TimeSpan.FromSeconds(30)); } return cachedData; } } -
記得將CacheDto加上Serializable的Attribute,標記為可序列化
[Serializable] public class CacheDto
{ public T Data { get; set; } public DateTime UpdateTime { get; set; } } -
重新整理頁面,可以發現在30秒內所取得的資料都會一模一樣,更新時間也相同,30秒之後才會更新,代表我們的Cache成功生效了!完成的程式碼請參考Github Branch: Integrate with redis
參考資料:
使用AOP,讓程式碼更加的乾淨!
在上一個段落中,我們整合了Redis作為我們網站的Cache,避免每次都需要重新產生資料,也減低了Server的負擔,但我們發現如果直接在程式碼每一段需要的地方都加上Cache的程式碼的話,不僅僅讓每一個Class的商業邏輯都變複雜,在測試上也不容易進行,因此我們可以透過Aop的方法來重構,撰寫通用的Inteceptor來實作Cache的機制,讓Cache的程式碼獨立在商業邏輯之外,只需要再要套用的Function上加上Attribute即可!
-
安裝Autofac.Extras.DynamicProxy2,支援Autofac的Aop套件
Install-Package Autofac.Extras.DynamicProxy2
-
建立CacheAttribute,並可指定Cache持續時間 (單位: 秒)
由於Autofac的Aop只能針對Class指定,但我們不一定這個Class的每個Method都希望被Cache,所以我們額外撰寫一個Attribute加在Method上,用來判斷這個Method是否需啟用快取,有加的才啟用。
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] sealed class CacheAttribute : Attribute { public CacheAttribute() { this.ExpireTime = 20 * 60; } public string CacheKey { get; set; } public int ExpireTime { get; set; } }
-
建立CacheInterceptor,用來共用的Cache邏輯
public class CacheInterceptor : IInterceptor { public ConnectionMultiplexer RedisConnection { get; set; } public CacheInterceptor(ConnectionMultiplexer connection) { this.RedisConnection = connection; } public void Intercept(IInvocation invocation) { //// 判斷目前方法是否有需要啟用Cache,若有加上標記表示需要 var attributes = invocation.MethodInvocationTarget.GetCustomAttributes(typeof(CacheAttribute), true); if (attributes.Count() > 0) { //// Cache key var key = string.Format("{0}.{1}.{2}", invocation.TargetType.FullName, invocation.MethodInvocationTarget.Name, JsonConvert.SerializeObject(invocation.Arguments)); //// Expire Time var expireTime = (attributes.First() as CacheAttribute).ExpireTime; IDatabase cache = this.RedisConnection.GetDatabase(); //// Cache是否存在,不存在產生新的 var result = cache.Get(key); if (result != null) { invocation.ReturnValue = result; return; } invocation.Proceed(); cache.Set(key, invocation.ReturnValue, TimeSpan.FromSeconds(expireTime)); } } }
-
在LongRunningService上,加上Cache的Attribute
[Intercept(typeof(CacheInterceptor))] public class LongRunningService : ILongRunningService { [Cache(ExpireTime = 30)] public CacheDto
> GetData(int min, int max) { Thread.Sleep(5000); var r = new Random(); var result = new List (); for (int i = 0; i < 10; i++) { result.Add(r.Next(min, max)); } return new CacheDto > { Data = result, UpdateTime = DateTime.Now }; } } -
在Autofac的註冊中,標記LongRunningService需要注入攔截器,並且註冊CacheInterceptor
builder.RegisterType
() .As () .EnableInterfaceInterceptors(); builder.RegisterType () .AsSelf(); -
重新執行網頁,可以看到網頁按照我們預期的,每30秒更新一次資料,完成的程式碼請參考Github Branch: Integrate with redis and aop
參考資料:
使用HangFire,將更新Cache的工作交給背景來執行
在上面的段落中,基本上已經完成了一個簡單的Cache機制雛形,但有沒有可能再更進一步呢? 如果我們可以不要在前端的程式中更新頁面,而是當Cache過期時,新增一個背景工作,交給background的Queue System來更新Cache,這麼一來,我們就可以保持前端的頁面永遠是非常快地回應給Client!接下來我就要向大家介紹如何使用HangFire這套Framework來執行背景的工作。
-
HangFire是一套Queue Framework,可以讓我們在背景執行指定的工作,而且非常簡單就能套用,我們先來進行HangFire的安裝
Install-Package HangFire Install-Package HangFire.Redis Install-Package HangFire.Autofac
-
修改自動產生HangFireConfig檔案,使用Redis作為儲存空間
public class HangFireConfig { private static AspNetBackgroundJobServer _server; public static void Start() { JobStorage.Current = new RedisStorage("localhost:6379", 3); _server = new AspNetBackgroundJobServer(); _server.Start(); } public static void Stop() { _server.Stop(); } }
-
連到http://localhost:1234/hangfire.axd,可以看到管理介面就代表安裝成功
-
新增一個RenewCacheAdapter,讓我們可以在背景中更新Cache,這邊主要是透過反射的機制,來動態執行撈取資料的Class取得資料並更新Cache
public class RenewCacheAdapter { public ILifetimeScope LifetimeScope { get; set; } public ConnectionMultiplexer RedisConnection { get; set; } public RenewCacheAdapter(ILifetimeScope lifetimeScope, ConnectionMultiplexer connection) { this.LifetimeScope = lifetimeScope; this.RedisConnection = connection; } public void RenewCache(string key, int expireTime, string typeName, string methodName, string argumentString) { IDatabase cache = this.RedisConnection.GetDatabase(); //// 若未過期,不執行更新 var result = cache.Get
(key); if (result.UpdateTime + TimeSpan.FromSeconds(expireTime) > DateTime.Now) { return; } //// 取得資料來源Class var targetType = Type.GetType(typeName); var methodInfo = targetType.GetMethod(methodName); var target = this.LifetimeScope.Resolve(targetType); //// 將參數轉換回原本的Type (因Json.Net會預設反序列化為Int64,但此處為Int32) var arguments = JsonConvert.DeserializeObject -
修改我們原本的CacheInterceptor,當Cache過期時,用寫入一個HangFire的更新Cache Job來取代直接更新Cache
public class CacheInterceptor : IInterceptor { public ConnectionMultiplexer RedisConnection { get; set; } public CacheInterceptor(ConnectionMultiplexer connection) { this.RedisConnection = connection; } public void Intercept(IInvocation invocation) { //// 判斷目前方法是否有需要啟用Cache,若有加上標記表示需要 var attributes = invocation.MethodInvocationTarget.GetCustomAttributes(typeof(CacheAttribute), true); if (attributes.Count() > 0) { //// Cache key var key = string.Format("{0}.{1}.{2}", invocation.TargetType.FullName, invocation.MethodInvocationTarget.Name, JsonConvert.SerializeObject(invocation.Arguments)); //// Expire Time var expireTime = (attributes.First() as CacheAttribute).ExpireTime; IDatabase cache = this.RedisConnection.GetDatabase(); //// 完全無資料時,直接產生 var result = cache.Get
(key); if (result == null) { invocation.Proceed(); var dataToCache = new CacheDto{ Data = invocation.ReturnValue, UpdateTime = DateTime.Now }; cache.Set(key, dataToCache, TimeSpan.FromHours(1)); return; } //// 快取過期時,產生一個背景更新的工作 //// 因背景後續才執行,因此此處可以馬上回應 if (result.UpdateTime + TimeSpan.FromSeconds(expireTime) < DateTime.Now) { BackgroundJob.Enqueue ( i => i.RenewCache(key, expireTime, invocation.TargetType.FullName, invocation.MethodInvocationTarget.Name, JsonConvert.SerializeObject(invocation.Arguments))); } //// 直接回傳舊資料 invocation.ReturnValue = result.Data; } } } -
最後在AutofacConfig中註冊所需要的Class
//// For website builder.RegisterType
() .As () .EnableInterfaceInterceptors(); //// For background job builder.RegisterType () .AsSelf(); builder.RegisterType () .AsSelf(); builder.RegisterType () .AsSelf(); -
重新執行程式,我們可以發現不論是否更新資料,每次的回應都相當快速,而在背景中也可以看到有任務被寫入,並使用HangFire執行進行Cache的更新
完成的程式碼請參考Github Branch: Integrate with hangfire
參考資料:
總結
本篇文章只是一個概念的雛型,若要真正實際應用到網站之中還有許多可以完善調整的地方,但可藉此來對一些想法做初步的驗證,也是非常的有趣,未來有機會還會分享更多類似的內容,希望可以和大家一起腦力激盪來討論一些有趣架構的實作,也歡迎大家分享自己的想法喔 ^_^