.NET提供了兩個獨立的緩存框架,一個是針對本地內存的緩存,另一個是針對分布式存儲的緩存。前者可以在不經過序列化的情況下直接將對象存儲在應用程序進程的內存中,后者則需要將對象序列化成字節數組並存儲到一個獨立的“中心數據庫”。對於分布式緩存,.NET提供了針對Redis和SQL Server的原生支持。(本篇提供的實例已經匯總到《ASP.NET Core 6框架揭秘-實例演示版》)
[S1101]基於內存的本地緩存(源代碼)
[S1102]基於Redis的分布式緩存(源代碼)
[S1103]基於SQL Server的分布式緩存(源代碼)
[S1101]基於內存的本地緩存
相較於針對數據庫和遠程服務調用這種IO操作來說,針對內存的訪問在性能上將獲得不只一個數量級的提升,所以將數據對象直接緩存在應用進程的內存中具有最佳的性能優勢。基於內存的緩存框架實現在NuGet包“Microsoft.Extensions.Caching.Memory”中,具體的緩存功能由IMemoryCache對象提供。由於緩存的數據直接存放在內存中,所以無須考慮序列化問題,對緩存數據的類型也就沒有任何限制。
緩存的操作主要是對緩存數據的讀和寫,這兩個基本操作都是由上面介紹的IMemoryCache對象來完成的。對於像ASP.NET這種支持依賴注入應用開發框架來說,采用注入的方式來使用IMemoryCache對象是推薦的編程方式。在如下所示的演示程序中,我們通過調用AddMemoryCache擴展方法將針對內存緩存的服務注冊添加到創建的ServiceCollection對象中,最終利用構建的IServiceProvider對象得到我們所需的IMemoryCache對象。
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; var cache = new ServiceCollection() .AddMemoryCache() .BuildServiceProvider() .GetRequiredService<IMemoryCache>(); for (int index = 0; index < 5; index++) { Console.WriteLine(GetCurrentTime()); await Task.Delay(1000); } DateTimeOffset GetCurrentTime() { if (!cache.TryGetValue<DateTimeOffset>("CurrentTime", out var currentTime)) { cache.Set("CurrentTime", currentTime = DateTimeOffset.UtcNow); } return currentTime; }
為了展現緩存的效果,我們將當前時間緩存起來。如上面的代碼片段所示,用於返回當前時間的GetCurrentTime方法在執行的時候會調用IMemoryCache對象的TryGetValue<T>方法,該方法根據指定的Key(“CurrentTime”)提取緩存的時間。如果通過該方法的返回值確定時間尚未被緩存,它會調用Set方法對當前時間予以緩存。我們的演示程序會以一秒的間隔五次調用這個GetCurrentTime,並將返回的時間輸出控制台上。由於使用了緩存,所以每次都會輸出相同的時間。
[S1102]基於Redis的分布式緩存
雖然采用基於本地內存緩存可以獲得最高的性能優勢,但對於部署在集群的應用程序無法確保緩存內容的一致性。為了解決這個問題,我們可以選擇將數據緩存在某個獨立的存儲中心,以便讓所有的應用實例共享同一份緩存數據,我們將這種緩存形式稱為分布式緩存。 .NET為分布式緩存提供了Redis和SQL Server這兩種原生的存儲形式。
Redis是目前較為流行的NoSQL數據庫,很多編程平台都將其作為分布式緩存的首選。由於演示程序運行在Windows系統下,所以我們使用與之完全兼容的Memurai來代替Redis。考慮到有的讀者可能沒有在Windows環境下體驗過Redis/Memurai,所以我們先簡單介紹Redis/Memurai如何安裝。Redis/Memurai最簡單的安裝方式就是采用Chocolatey命令行(Chocolatey是Windows平台下一款優秀的軟件包管理工具),Chocolatey的官方站點(https://chocolatey.org/install)提供了各種安裝方式。在確保Chocolatey被正常安裝的情況下,我們可以執行“choco install redis-64”命令安裝或者升級64位的Redis,從圖11-2可以看出我們真正安裝的是用來代替Redis的Memurai開發版。
Redis/Memurai服務器的啟動也很簡單,我們只需要以命令行的形式執行“memurai”命令即可。如果在執行該命令之后看到圖11-3所示的輸出,則表示本地的Redis/Memurai服務器被正常啟動,輸出的結果會指明服務器采用的網絡監聽端口(默認6379)和進程號。
我們接下來對上面演示的實例進行簡單的修改,將基於內存的本地緩存切換到針對Redis數據庫的分布式緩存。不論采用Redis、SQL Server還是其他的分布式存儲方式,緩存的讀和寫都是通過IDistributedCache對象完成的。Redis分布式緩存承載於 “Microsoft.Extensions.Caching.Redis”這個NuGet包中,我們需要手動添加針對該NuGet包的依賴。
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; var cache = new ServiceCollection().AddDistributedRedisCache(options => { options.Configuration = "localhost"; options.InstanceName = "Demo"; }) .BuildServiceProvider() .GetRequiredService<IDistributedCache>(); for (int index = 0; index < 5; index++) { Console.WriteLine(await GetCurrentTimeAsync()); await Task.Delay(1000); } async Task<DateTimeOffset> GetCurrentTimeAsync() { var timeLiteral = await cache.GetStringAsync("CurrentTime"); if (string.IsNullOrEmpty(timeLiteral)) { await cache.SetStringAsync("CurrentTime", timeLiteral = DateTimeOffset.UtcNow.ToString()); } return DateTimeOffset.Parse(timeLiteral); }
從上面的代碼片段可以看出,分布式緩存和內存緩存在總體編程模式上是一致的,我們需要先完成針對IDistributedCache服務的注冊,然后利用依賴注入框架提供該服務對象來進行緩存數據的讀和寫。IDistributedCache服務的注冊是通過調用IServiceCollection接口的AddDistributedRedisCache方法來完成的。我們在調用這個方法時提供了一個RedisCacheOptions對象,並利用它的Configuration和InstanceName屬性設置Redis數據庫的服務器與實例名稱。
由於采用的是本地的Redis服務器,所以我們將Configuration屬性設置為localhost。其實Redis數據庫並沒有所謂的實例的概念,RedisCacheOptions類型的InstanceName屬性的目的在於當多個應用共享同一個Redis數據庫時,緩存數據可以利用它進行區分。當緩存數據被保存到Redis數據庫中的時候,對應的Key以InstanceName為前綴。應用程序啟動后(確保Redis服務器被正常啟動),如果我們利用瀏覽器來訪問它,依然可以得到與圖1類似的輸出。
對於基於內存的本地緩存來說,我們可以將任何類型的數據置於緩存之中,但是分布式緩存涉及網絡傳輸和持久化存儲,置於緩存中的數據類型只能是字節數組,所以我們需要自行負責對緩存對象的序列化和反序列化工作。如上面的代碼片段所示,我們先將表示當前時間的DateTime對象轉換成字符串,然后采用UTF-8編碼進一步轉換成字節數組。我們調用IDistributedCache接口的SetAsync方法緩存的數據是最終的字節數組。我們也可以直接調用SetStringAsync擴展方法將字符串編碼為字節數組。在讀取緩存數據時,我們調用的是IDistributedCache接口的GetStringAsync方法,它會將字節數組轉換成字符串。
緩存數據在Redis數據庫中是以散列(Hash)的形式存放的,對應的Key會將設置的InstanceName屬性作為前綴。為了查看在Redis數據庫中究竟存放了哪些數據,我們可以按照圖4所示的形式執行Redis命令獲取存儲的數據。從輸出結果可以看出存入Redis數據庫的不僅包括指定的緩存數據(Sub-Key為data),還包括其他兩組針對該緩存條目的描述信息,對應的Sub-Key分別為absexp和sldexp,表示緩存的絕對過期時間(Absolute Expiration Time)和滑動過期時間(Sliding Expiration Time)。
[S1103]基於SQL Server的分布式緩存
除了使用Redis這種主流的NoSQL數據庫來支持分布式緩存,還可以使用關系型數據庫SQL Server。針對SQL Server的分布式緩存實現在NuGet包“Microsoft.Extensions.Caching.SqlServer”中,我們需要先確保該NuGet包被正常安裝到演示的應用程序中。針對SQL Server的分布式緩存實際上就是將表示緩存數據的字節數組存放在SQL Server數據庫的某個具有固定結構的數據表中,所以我們需要先創建這樣一個緩存表。該表可以通過dotnet-sql-cache命令行工具進行創建。如果該命令行工具尚未安裝,我們可以執行“dotnet tool install --global dotnet-sql-cache”進行安裝。
具體來說,存儲緩存數據的表可以采用命令行的形式執行“dotnet sql-cache create”命令來創建。執行這個命令應該指定的參數可以按照如下形式通過執行“dotnet sql-cache create --help”命令來查看。從圖5可以看出,該命令需要指定三個參數,它們分別表示緩存數據庫的連接字符串、緩存表的Schema和名稱。
圖5 dotnet sql-cache create命令的幫助文檔
接下來只需要以命令行的形式執行“dotnet sql-cache create”命令就可以在指定的數據庫中創建緩存表。對於演示的實例來說,可以按照圖6所示的方式執行“dotnet sql-cache create”命令,該命令會在本機一個名為DemoDB的數據庫中(數據庫需要預先創建好)創建一個名為AspnetCache的緩存表,該表采用dbo作為Schema。
圖6 執行“dotnet sql-cache create”命令創建緩存表
在所有的准備工作完成之后,我們只需要對上面的程序做如下修改就可以將緩存存儲方式從Redis數據庫切換到針對SQL Server的數據庫。由於采用的同樣是分布式緩存,所以針對緩存數據的設置和提取的代碼不用做任何改變,我們需要修改的地方僅僅是服務注冊部分。如下面的代碼片段所示,我們調用IServiceCollection接口的AddDistributedSqlServerCache擴展方法完成了對應的服務注冊。在調用這個方法的時候,我們通過設置SqlServerCacheOptions對象三個屬性的方式指定了緩存數據庫的連接字符串、緩存表的Schema和名稱。
public class Program { public static void Main() { Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(builder => builder.ConfigureServices(svcs => svcs.AddDistributedSqlServerCache(options => { options.ConnectionString = "server=.;database=demodb;uid=sa;pwd=password"; options.SchemaName = "dbo"; options.TableName = "AspnetCache"; })) .Configure(app => app.Run(async context => { var cache = context.RequestServices.GetRequiredService<IDistributedCache>(); var currentTime = await cache.GetStringAsync("CurrentTime"); if (null == currentTime) { currentTime = DateTime.Now.ToString(); await cache.SetAsync("CurrentTime", Encoding.UTF8.GetBytes(currentTime)); } await context.Response.WriteAsync($"{currentTime}({DateTime.Now})"); }))) .Build() .Run(); } }
若要查看最終存入SQL Server數據庫中的緩存數據,我們只需要在數據庫中查看對應的緩存表即可。對於演示實例緩存的時間戳,它會以圖7所示的形式保存在我們創建的緩存表(AspnetCache)中。與基於Redis數據庫的存儲方式類似,與緩存數據的值一並存儲的還包括緩存的過期信息。
圖7 存儲在緩存表中的數據