.NET提供了两个独立的缓存框架,一个是针对本地内存的缓存,另一个是针对分布式存储的缓存。前者可以在不经过序列化的情况下直接将对象存储在应用程序进程的内存中,后者则需要将对象序列化成字节数组并存储到一个独立的“中心数据库”。对于分布式缓存,.NET提供了针对Redis和SQL Server的原生支持。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)
[S1101]基于内存的本地缓存[S1101]基于内存的本地缓存(源代码)
[S1102]基于Redis的分布式缓存(源代码)
[S1103]基于SQL Server的分布式缓存(源代码)
相较于针对数据库和远程服务调用这种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,并将返回的时间输出控制台上。由于使用了缓存,所以每次都会输出相同的时间。
图1 缓存在内存中的时间
虽然采用基于本地内存缓存可以获得最高的性能优势,但对于部署在集群的应用程序无法确保缓存内容的一致性。为了解决这个问题,我们可以选择将数据缓存在某个独立的存储中心,以便让所有的应用实例共享同一份缓存数据,我们将这种缓存形式称为分布式缓存。 .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开发版。
图2 安装Redis/Memurai
Redis/Memurai服务器的启动也很简单,我们只需要以命令行的形式执行“memurai”命令即可。如果在执行该命令之后看到图11-3所示的输出,则表示本地的Redis/Memurai服务器被正常启动,输出的结果会指明服务器采用的网络监听端口(默认6379)和进程号。
图3 以命令行的形式启动Memurai服务器
我们接下来对上面演示的实例进行简单的修改,将基于内存的本地缓存切换到针对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)。
图4 查看Redis数据库中存放的数据
除了使用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 存储在缓存表中的数据