.NET Core針對緩存提供了很好的支持 ,咱們不只能夠選擇將數據緩存在應用進程自身的內存中,還能夠採用分佈式的形式將緩存數據存儲在一個「中心數據庫」中。對於分佈式緩存,.NET Core提供了針對Redis和SQL Server的原生支持。除了這個獨立的緩存系統以外,ASP.NET Core還藉助一箇中間件實現了「響應緩存」,它會按照HTTP緩存規範對整個響應實施緩存。不過按照慣例,在對緩存進行系統介紹以前,咱們仍是先經過一些簡單的實例演示感知一下若是在一個ASP.NET Core應用中如何使用緩存。redis
目錄
1、將數據緩存在內存中
2、基於Redis的分佈式緩存
3、基於SQL Server的分佈式緩存
4、緩存整個HTTP響應sql
1、將數據緩存在內存中
與針對數據庫和遠程服務調用這種IO操做來講,應用針對內存的訪問性能將提供不止一個數量級的提高,因此將數據直接緩存在應用進程的內容中天然具備最佳的性能優點。與基於內存的緩存相關的應用編程接口定義在NuGet包「Microsoft.Extensions.Caching.Memory」中,具體的緩存實如今一個名爲MemoryCache的服務對象中,後者是咱們對全部實現了IMemoryCache接口的全部類型以及對應對象的統稱。因爲是將緩存對象直接置於內存之中,中間並不涉及持久化存儲的問題,天然也就無需考慮針對緩存對象的序列化問題,因此這種內存模式支持任意類型的緩存對象。shell
針對緩存的操做不外乎對緩存數據的存與取,這兩個基本的操做都由上面介紹的這個MemoryCache對象來完成。若是咱們在一個ASP.NET Core應用對MemoryCache服務在啓動時作了註冊,咱們就能夠在任何地方獲取該服務對象設置和獲取緩存數據,因此針對緩存的編程是很是簡單的。數據庫
1: public class Program
2: {
3: public static void Main()
4: {
5: new WebHostBuilder()
6: .UseKestrel()
7: .ConfigureServices(svcs => svcs.AddMemoryCache())
8: .Configure(app => app.Run(async context =>
9: {
10: IMemoryCache cache = context.RequestServices.GetRequiredService<IMemoryCache>();
11: DateTime currentTime;
12: if (!cache.TryGetValue<DateTime>("CurrentTime", out currentTime))
13: {
14: cache.Set("CurrentTime", currentTime = DateTime.Now);
15: }
16: await context.Response.WriteAsync($"{currentTime}({DateTime.Now})");
17: }))
18: .Build()
19: .Run();
20: }
21: }
在上面這個演示程序中,咱們在WebHostBuilder的ConfigureServices方法中經過調用ServiceCollection的擴展方法AddMemoryCache完成了針對MemoryCache的服務註冊。在WebHostBuilder的Configure方法中,咱們經過調用ApplicationBuilder的Run方法註冊了一箇中間件對請求作了簡單的響應。咱們先從當前HttpContext中獲得對應的ServiceProvider,並利用後者獲得MemoryCache對象。咱們接下來調用MemoryCache的Set方法將當前時間緩存起來(若是還沒有緩存),並指定一個惟一的Key(「CurrentTime」)。經過指定響應的Key,咱們能夠調用另外一個名爲TryGetValue<T>的方法獲取緩存的對象。咱們最終寫入的響應內容其實是緩存的時候和當前實施的時間。因爲緩存的是當前時間,因此當咱們經過瀏覽器訪問該應用的時候,顯示的時間在緩存過時以前老是不變的編程
雖然基於內存的緩存具備最高的性能,可是因爲它其實是將緩存數據存在承載ASP.NET Core應用的Web服務上,對於部署在集羣式服務器中的應用會出現緩存數據不一致的狀況。對於這種部署場景,咱們須要將數據緩存在某一個獨立的存儲中心,以便讓全部的Web服務器共享同一份緩存數據,咱們將這種緩存形式稱爲「分佈式緩存」。ASP.NET Core爲分佈式緩存提供了兩種原生的存儲形式,一種是基於NoSQL的Redis數據庫,另外一種則是微軟自家關係型數據庫SQL Server。json
2、基於Redis的分佈式緩存
Redis數目前較爲流行NoSQL數據庫,不少的編程平臺都將它做爲分佈式緩存的首選,接下來咱們來演示如何在一個ASP.NET Core應用中如何採用基於Redis的分佈式緩存。考慮到一些人可能尚未體驗過Redis,因此咱們先來簡單介紹一下如何安裝Redis。Redis最簡單的安裝方式就是採用Chocolatey(https://chocolatey.org/) 命令行,後者是Windows平臺下一款優秀的軟件包管理工具(相似於NPM)。數組
1: PowerShell prompt :
2: iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex
3:
4: CMD.exe:
5: @powershell -NoProfile -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"
咱們既能夠採用PowerShell (要求版本在V3以上)命令行或者普通CMD.exe命令行來安裝Chocolatey ,具體的命令如上所示。在確保Chocolatey 被本地正常安裝狀況下,咱們能夠執行執行以下的命令安裝或者升級64位的Redis。瀏覽器
1: C:\>choco install redis-64
2: C:\>choco upgrade redis-64
Redis服務器的啓動也很簡單,咱們只須要以命令行的形式執行redis-server命令便可。若是在執行該命名以後看到以下圖所示的輸出,則表示本地的Redis服務器被正常啓動,輸出的結果會指定服務器採用的網絡監聽端口。緩存
接下來咱們會對上面演示的實例進行簡單的修改,將基於內存的本地緩存切換到針對Redis數據庫的分佈式緩存。針對Redis的分佈式緩存實如今NuGet包「Microsoft.Extensions.Caching.Redis」之中,因此咱們須要確保該NuGet包被正常安裝。不論採用Redis、SQL Server仍是其餘的分佈式存儲方式,針對分佈式緩存的操做都實如今DistributedCache這個服務對象向,該服務對應的接口爲IDistributedCache。服務器
1: public class Program
2: {
3: public static void Main()
4: {
5: new WebHostBuilder()
6: .UseKestrel()
7: .ConfigureServices(svcs => svcs.AddDistributedRedisCache(options =>
8: {
9: options.Configuration = "localhost";
10: options.InstanceName = "Demo";
11: }))
12: .Configure(app => app.Run(async context =>
13: {
14: var cache = context.RequestServices.GetRequiredService<IDistributedCache>();
15: string currentTime = await cache.GetStringAsync("CurrentTime");
16: if (null == currentTime)
17: {
18: currentTime = DateTime.Now.ToString();
19: await cache.SetAsync("CurrentTime", Encoding.UTF8.GetBytes(currentTime));
20: }
21: await context.Response.WriteAsync($"{currentTime}({DateTime.Now})");
22: }))
23: .Build()
24: .Run();
25: }
26: }
從上面的代碼片斷能夠看出,針對分佈式緩存和內存緩存在整體編程模式上是一致的,咱們須要先註冊針對DistributedCache的服務註冊,可是利用依賴注入機制提供該服務對象來進行緩存數據的設置和緩存。咱們調用IServiceCollection的另外一個擴展方法AddDistributedRedisCache註冊DistributedCache服務,在調用這個方法的時候藉助於RedisCacheOptions這個對象的Configuration和InstanceName屬性設置Redis數據庫的服務器和實例名稱。因爲採用的是本地的Redis服務器,因此咱們將前者設置爲「localhost」。其實Redis數據庫並無所爲的實例的概念,RedisCacheOptions的InstanceName屬性的目的在於當多個應用共享同一個Redis數據庫的時候,緩存數據能夠利用它來區分,當緩存數據被保存到Redis數據庫中的時候,對應的Key會以它爲前綴。修改後的應用啓動後(確保Redis服務器被正常啓動),若是咱們利用瀏覽器來訪問它,依然會獲得與前面相似的輸出。
對於基於內存的本地緩存來講,咱們能夠將任何類型的數據置於緩存之中,可是對於分佈式緩存來講,因爲涉及到網絡傳輸甚至是持久化存儲,放到緩存中的數據類型只能是字節數組,因此咱們須要自行負責對緩存對象的序列化和反序列化工做。如上面的代碼片斷所示,咱們先將表示當前時間的DateTime對象轉換成字符串,而後採用UTF-8編碼進一步轉換成字節數組,最終調用DistributedCache的SetAsync方法將後者緩存起來。實際上咱們也能夠直接調用另外一個擴展方法SetStringAsync,它會負責將字符串編碼爲字節數組。在獲取緩存的時候,咱們調用的是DistributedCache的GetStringAsync方法,它會將字節數組轉換成字符串。
緩存數據在Redis數據庫中是以散列(Hash)的形式存放的,對應的Key會將設置的InstanceName做爲前綴(若是進行了設置)。爲了查看究竟存放了哪些數據在Redis數據庫中,咱們能夠按照如圖3所示的形式執行Redis命名來獲取存儲的數據。從下圖呈現的輸出結果咱們不難看出,存入的不只僅包括咱們指定的緩存數據(Sub-Key爲「data」)以外,還包括其餘兩組針對該緩存條目的描述信息,對應的Sub-Key分別爲「absexp」和「sldexp」,表示緩存的絕對過時時間(Absolute Expiration Time)和滑動過時時間(Slidding Expiration Time)。
3、基於SQL Server的分佈式緩存
除了使用Redis這種主流的NoSQL數據庫來支持分佈式緩存,微軟在設計分佈式緩存時也沒有忘記自家的關係型數據庫採用SQL Server。針對SQL Server的分佈式緩存實如今「Microsoft.Extensions.Caching.SqlServer」這個NuGet包中,咱們先得確保該NuGet包被正常裝到演示的應用中。
所謂的針對SQL Server的分佈式緩存,實際上就是將標識緩存數據的字節數組存放在SQL Server數據庫中某個具備固定結構的數據表中,由於咱們得先來建立這麼一個緩存表,該表能夠藉助一個名爲sql-cache 的工具來建立。在執行sql-cache 工具建立緩存表以前,咱們須要在project.json文件中按照以下的形式爲這個工具添加相應的NuGet包「Microsoft.Extensions.Caching.SqlConfig.Tools」。
1: {
2: …
3: "tools": {
4: "Microsoft.Extensions.Caching.SqlConfig.Tools": "1.1.0-preview4-final"
5: }
6: }
當針對上述這個NuGet包復原(Restore)以後,咱們能夠執行「dotnet sql-cache create」命令來建立,至於這個執行這個命令應該指定怎樣的參數,咱們能夠按照以下的形式經過執行「dotnet sql-cache create --help」命令來查看。從下圖能夠看出,該命名須要指定三個參數,它們分別表示緩存數據庫的連接字符串、緩存表的Schema和名稱。
接下來咱們只須要在演示應用所在的項目根目錄(project.json文件所在的目錄)下執行dotnet sql-cache create就能夠在指定的數據庫建立緩存表了。對於咱們演示的實例來講,咱們按照下圖所示的方式執行這dotnet sql-cache create命令行在本機一個名爲demodb的數據庫中建立了一個名爲AspnetCache的緩存表,該表採用dbo做爲Schema。
在全部的準備工做完成以後,咱們只須要對上面的程序作以下的修改便可將針對Redis數據庫的緩存切換到針對SQL Server數據庫的緩存。因爲採用的一樣是分佈式緩存,因此針對緩存數據的設置和提取的代碼不用作任何改變,咱們須要修改的地方僅僅是服務註冊部分。以下面的代碼片斷所示,咱們在WebHostBuilder的ConfigureServices方法中調用IServiceCollection的擴展方法AddDistributedSqlServerCache完成了對應的服務註冊。在調用這個方法的時候,咱們經過設置SqlServerCacheOptions對象的三個屬性的方式指定了緩存數據庫的連接字符串和緩存表的Schema和名稱。
1: public class Program
2: {
3: public static void Main()
4: {
5: new WebHostBuilder()
6: .UseKestrel()
7: .ConfigureServices(svcs => svcs.AddDistributedSqlServerCache(options =>
8: {
9: options.ConnectionString = "server=.;database=demodb;uid=sa;pwd=password";
10: options.SchemaName = "dbo";
11: options.TableName = "AspnetCache";
12: }))
13: .Configure(app => app.Run(async context =>
14: {
15: var cache = context.RequestServices.GetRequiredService<IDistributedCache>();
16: string currentTime = await cache.GetStringAsync("CurrentTime");
17: if (null == currentTime)
18: {
19: currentTime = DateTime.Now.ToString();
20: await cache.SetAsync("CurrentTime", Encoding.UTF8.GetBytes(currentTime));
21: }
22: await context.Response.WriteAsync($"{currentTime}({DateTime.Now})");
23: }))
24: .Build()
25: .Run();
26: }
27: }
若是想看看最終存入SQL Server數據庫中的究竟包含哪些緩存數據,咱們只須要直接在所在數據庫中查看對應的緩存表了。對於演示實例緩存的數據,它會如下圖所示的形式保存在咱們建立的緩存表(AspnetCache)中,與基於Redis的緩存相似,與指定緩存數據的值一併存儲的還包括緩存的過時信息。
4、緩存整個HTTP響應
上面演示的兩種緩存都要求咱們利用註冊的服務對象以手工的方式存儲和提取具體的緩存數據,而接下來咱們演示的緩存則再也不基於某個具體的緩存數據,而是將服務端最終生成的響應主體內容予以緩存,咱們將這種緩存形式稱爲響應緩存(Response Caching)。標準的HTTP規範,不管是HTTP 1.0+仍是HTTP 1.1,都會緩存作了詳細的規定,這是響應規範的理論機制和指導思想。咱們將在後續內容中詳細介紹HTTP緩存,在這以前咱們先經過一個簡單的實例來演示一下整個響應內容是如何藉助一個名爲ResponseCachingMiddleware中間件被緩存起來的。該中間件由「Microsoft.AspNetCore.ResponseCaching」這個NuGet包提供。
經過一樣是採用基於時間的緩存場景,爲此咱們編寫了以下這個簡單的程序。咱們在WebHostBuilder的ConfigureServices方法中調用了IServiceCollection接口的擴展方法AddResponseCaching註冊了中間件ResponseCachingMiddleware依賴的全部的服務,而這個中間件的註冊則經過調用IApplicationBuilder接口的擴展方法UseResponseCaching完成。
1: public class Program
2: {
3: public static void Main()
4: {
5: new WebHostBuilder()
6: .UseKestrel()
7: .ConfigureServices(svcs=>svcs.AddResponseCaching())
8: .Configure(app => app
9: .UseResponseCaching()
10: .Run(async context => {
11: context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
12: {
13: Public = true,
14: MaxAge = TimeSpan.FromSeconds(3600)
15: };
16:
17: string utc = context.Request.Query["utc"].FirstOrDefault()??"";
18: bool isUtc = string.Equals(utc, "true", StringComparison.OrdinalIgnoreCase);
19: await context.Response.WriteAsync(isUtc? DateTime.UtcNow.ToString(): DateTime.UtcNow.ToString());
20: }))
21: .Build()
22: .Run();
23: }
24: }
對於最終實現的請求處理邏輯來講,咱們僅僅是爲響應添加了一個Cache-Control報頭,並將它的值設置爲「public, max-age=3600」(public表示緩存的是能夠被全部用戶共享的公共數據,而max-age則表示過去時限,單位爲秒)。真正寫入響應的主體內容就是當前時間,不給過咱們會根據請求的查詢字符串「utc」決定採用普通時間仍是UTC時間。
要證實整個響應的內容是否被被緩存起來,咱們只須要驗證在緩存過時以前具備相同路徑的多個請求對應的響應是否具備相同的主體內容,爲此咱們採用Fiddler來生髮送的請求並攔截響應的內容。以下所示的兩組請求和響應是在不一樣時間發送的,咱們能夠看出響應的內容是徹底一致的。因爲請求發送的時間不一樣,因此返回的緩存副本的「年齡」(對應於響應報頭Age)也是不一樣的。
1: GET http://localhost:5000/ HTTP/1.1
2: User-Agent: Fiddler
3: Host: localhost:5000
4:
5: HTTP/1.1 200 OK
6: Date: Sun, 12 Feb 2017 13:02:23 GMT
7: Content-Length: 20
8: Server: Kestrel
9: Cache-Control: public, max-age=3600
10: Age: 82
11:
12: 2/12/2017 1:02:23 PM
13:
14:
15: GET http://localhost:5000/ HTTP/1.1
16: User-Agent: Fiddler
17: Host: localhost:5000
18:
19: HTTP/1.1 200 OK
20: Date: Sun, 12 Feb 2017 13:02:23 GMT
21: Content-Length: 20
22: Server: Kestrel
23: Cache-Control: public, max-age=3600
24: Age: 85
25:
26: 2/12/2017 1:02:23 PM
上面這個兩個請求的URL並無攜帶「utc」查詢字符串,因此返回的是一個非UTC時間,接下來咱們採用相同的方式生成一個試圖返回UTC時間的請求。從下面給出的請求和響應的內容咱們能夠看出,雖然請求攜帶了查詢字符串「utc=true」,可是返回的依然是以前緩存的時間。因爲此可見,ResponseCachingMiddleware中間件在默認狀況下是針對請求的路徑對響應實施緩存的,它會忽略請求URL攜帶的查詢字符串,這顯然不是咱們但願看到的結果。
1: GET http://localhost:5000/?utc=true HTTP/1.1
2: User-Agent: Fiddler
3: Host: localhost:5000
4:
5: HTTP/1.1 200 OK
6: Date: Sun, 12 Feb 2017 13:02:23 GMT
7: Content-Length: 20
8: Server: Kestrel
9: Cache-Control: public, max-age=3600
10: Age: 474
11:
12: 2/12/2017 1:02:23 PM
按照REST的原則,URL是網路資源的標識,可是資源的表現形式(Representation)會由一些參數來決定,這些參數能夠體現爲查詢字符串,也能夠體現爲一些請求報頭,好比Language報頭決定資源的描述語言,Content-Encoding報頭決定資源採用的編碼方式。所以針對響應的緩存不該該只考慮請求的路徑,還應該綜合考慮這些參數。
對於演示的這個實例來講,咱們但願將查詢字符串「utc」歸入緩存考慮的範疇,這能夠利用一個名爲ResponseCachingFeature的特性來完成,該特性對應的接口爲IResponseCachingFeature。以下面的代碼片斷所示,在將當前時間寫入響應以後,咱們獲得這個特性並設置了它的VaryByQueryKeys屬性,該屬性包含一組決定輸出緩存的查詢字符串名稱,咱們將查詢字符「utc」添加到這個列表中。
1: public class Program
2: {
3: public static void Main()
4: {
5: new WebHostBuilder()
6: .UseKestrel()
7: .ConfigureServices(svcs=>svcs.AddResponseCaching())
8: .Configure(app => app
9: .UseResponseCaching()
10: .Run(async context => {
11: …
12: var feature = context.Features.Get<IResponseCachingFeature>();
13: feature.VaryByQueryKeys = new string[] { "utc" };
14: }))
15: .Build()
16: .Run();
17: }
18: }