.NET Core針對緩存提供了很好的支持 ,我們不僅可以選擇將數據緩存在應用進程自身的內存中,還可以采用分布式的形式將緩存數據存儲在一個“中心數據庫”中。對于分布式緩存,.NET Core提供了針對Redis和SQL Server的原生支持。除了這個獨立的緩存系統之外,ASP.NET Core還借助一個中間件實現了“響應緩存”,它會按照HTTP緩存規范對整個響應實施緩存。不過按照慣例,在對緩存進行系統介紹之前,我們還是先通過一些簡單的實例演示感知一下如果在一個ASP.NET Core應用中如何使用緩存。
目錄
一、將數據緩存在內存中
二、基于Redis的分布式緩存
三、基于SQL Server的分布式緩存
四、緩存整個HTTP響應
一、將數據緩存在內存中
與針對數據庫和遠程服務調用這種IO操作來說,應用針對內存的訪問性能將提供不止一個數量級的提升,所以將數據直接緩存在應用進程的內容中自然具有最佳的性能優勢。與基于內存的緩存相關的應用編程接口定義在NuGet包“Microsoft.Extensions.Caching.Memory”中,具體的緩存實現在一個名為MemoryCache的服務對象中,后者是我們對所有實現了IMemoryCache接口的所有類型以及對應對象的統稱。由于是將緩存對象直接置于內存之中,中間并不涉及持久化存儲的問題,自然也就無需考慮針對緩存對象的序列化問題,所以這種內存模式支持任意類型的緩存對象。
針對緩存的操作不外乎對緩存數據的存與取,這兩個基本的操作都由上面介紹的這個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。
二、基于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)。
三、基于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的緩存類似,與指定緩存數據的值一并存儲的還包括緩存的過期信息。
四、緩存整個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: }
文章列表