十、Redis 的性能增强
缓存是提高应用性能的常用技术。 通常,我们会在内容分发网络(CDN)、HTTP 缓存和数据库缓存中遇到缓存。 缓存通过最小化访问底层较慢的数据存储层的需求来提高数据检索性能。 在本章中我们将学习的缓存技术是内存缓存和分布式缓存。
本章将涵盖以下主题:
- ASP 中的内存缓存.NET Core
- 分布式缓存
- 设置和运行 Redis
- 在 ASP 中实现 Redis。 核心网 5
技术要求
以下是你完成本章所需要的东西:
- Visual Studio 2019, Visual Studio for Mac,或 Rider
- Redis
AnotherRedisDeskTopManager
,见https://www.electronjs.org/apps/anotherredisdesktopmanager
下面是该存储库的最终代码:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter10的链接。
ASP 的内存缓存 NET Core
使用内存缓存允许开发人员将数据存储在服务器的资源中,特别是内存中。 因此,它通过删除对外部数据源的不必要的 HTTP 请求来帮助开发人员提高性能。
在 ASP 中实现内存缓存.NET Core 非常简单。 但是,我们不会在我们的应用中应用它。 我们将选择一种可扩展的缓存方式,即分布式缓存。 我们将只看如何实现内存缓存的部分,以便您有一个想法。
在 ASP 中启用内存缓存 NET Core
重复一遍,我们不会将本节中的代码应用到我们的应用中。 无论如何,你可以在Startup.cs
的ConfigureServices
中启用内存缓存:
public void ConfigureServices(IServiceCollection services)
{
…
services.AddMemoryCache();
}
方法在。net 中添加了一个非分布式内存实现。 您可以开始使用内存缓存,而不安装任何 NuGet 包。 然后将IMemoryCache
注入到需要缓存的控制器中:
[Route("api/[controller]")]
[ApiController]
public class CacheController : ControllerBase
{
private readonly IMemoryCache _memoryCache;
public CacheController(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
}
在从Microsoft.Extensions.Caching.Memory
命名空间注入IMemoryCache
之后,您就可以开始使用它了。 下面的代码块只检查缓存中是否存在蛋糕列表,如果是true
则返回。 否则,它将使用服务并存储结果:
[HttpGet("{cakeName}")]
public async Task<List<string>> Get(string
cakeName)
{
var cacheKey = cakeName.ToLower();
if (!_memoryCache.TryGetValue(cacheKey, out
List<string> cakeList))
{
cakeList = await Service.GetCakeList(
cakeName);
var cacheExpirationOptions =
new MemoryCacheEntryOptions
{
AbsoluteExpiration =
DateTime.Now.AddHours(6),
Priority = CacheItemPriority.
Normal,
SlidingExpiration =
TimeSpan.FromMinutes(5)
};
_memoryCache.Set(cacheKey, cakeList,
cacheExpirationOptions);
}
return cakeList;
}
您还会注意到在代码块中缓存中存在过期。 AbsoluteExpiration
表示一个确定的到期日期,而SlidingExpiration
用于监视缓存的非活动状态,或者仅仅是将它最后一次使用的时间放置。
虽然内存缓存消耗了它的服务器的资源,但内存缓存比分布式缓存快,因为它是物理上连接到服务器上的,但对于大型和多 web 服务器并不理想。
这里有一条建议。 在运行一个解决方案的多个实例时,不建议使用内存缓存,因为数据将不一致。 在多台服务器上工作时,有一种更好的缓存方法,这将在下一节中讨论。
分布式缓存
分布式缓存或全局缓存是具有专用网络的单实例或一组缓存服务器。 当应用到达分布式缓存时,如果与应用请求相关的缓存数据不存在,请求将重定向到数据库来查询数据。 否则,分布式缓存将只响应应用所需的数据。
这是两个服务器共享同一个分布式缓存实例的图:
图 10.1 -分布式缓存
上图显示了来自两个服务器的请求在决定是否从数据库中查询之前首先访问 Redis 缓存。
如果您的服务中的一个崩溃会发生什么? 实际上什么都没有,因为每个人都将查询分布式缓存。 因为缓存是分布式的,它会维护数据的一致性。 我们可以把所有的信息和所有头疼的事情都转移到分布式缓存中,大部分时候是 Redis。 分布式缓存比内存中慢,但更准确。
需要分布式缓存的原因之一是为了获得更高的精度。 例如,如果服务器崩溃,它不会把它的数据带到坟墓里。 这种方式更有弹性。
另一个原因是你可以独立地扩展分布式缓存或 Redis 缓存。 你可以独立地扩展 Redis 实例,同时保持你的 web 服务正常运行,而不使用他们的资源缓存。
设置和运行 Redis
Redis 官方支持 Linux 和 macOS,但不支持 Windows,因为工程师编写的 Redis 使用 BSD Unix。 Windows 端口是由一些被称为Microsoft Open Tech 小组的志愿者开发人员编写的。
让我们在 Windows、macOS 和 Ubuntu 上安装 Redis。 以下步骤取决于您的操作系统。
Windows 用户
-
Go to https://github.com/microsoftarchive/redis/releases/tag/win-3.0.504 to download the installer of Redis for Windows:
图 10.2——Redis MSI 安装程序和 ZIP 文件
-
Download and extract the Redis ZIP. Double-click the
redis-server
file. Allow the permission dialog box that will pop up by accepting Yes. The Redis instance will automatically start.检查安装是否完成,在终端中执行如下命令:
redis-cli ping
redis-cli
是 Redis 功能的 CLI。 您应该看到来自终端的pong
响应。
下面是安装 Redis 的另一种方法,使用msi
文件从下载链接。
下载并安装msi
文件只需点击它。 允许通过接受Yes将弹出的权限对话框。 Redis 实例将自动启动。
检查安装是否完成,在终端中执行如下命令:
redis-cli ping
redis-cli
是 Redis 功能的 CLI。 您应该看到来自终端的pong
响应:
图 10.3 -解压后的文件和 Windows 终端上运行的 Redis 实例
这里是从 Windows 的 ZIP 文件和 CMD 中提取的文件,其中显示了点击redis-server
文件后的 Redis 图像。
如果你正在考虑使用 Chocolatey 包管理器来安装 Redis,那么在写这篇文章的时候,这个 URL 是坏的。 我收到一个错误说404 没有找到。
就是这样。 Redis 现在已经安装在 Windows 10 系统上。
用于 macOS 用户
你可以快速安装 Redis 在 Mac 上使用brew
:
- 首先,通过运行以下命令更新
brew
: -
接下来,我们通过运行以下命令安装 Redis:
brew install redis
-
然后,让我们运行命令启动已安装的 Redis:
brew services start redis
-
Now run the following command to check whether Redis is running and reachable:
redis-cli ping
redis-cli
是 Redis 功能的 CLI。 您应该看到来自 Terminal 的pong
响应。注意:
使用
brew
的 Redis 安装工作在 macOS 大苏尔,这是自最初的 macOS 以来最大的变化。
就是这样。 Redis 现在已经安装在 macOS 上了。
适用于 Linux 或 Ubuntu 用户
在 Linux 下安装 Redis 很简单:
- 让我们首先通过运行以下命令来更新我们的资源:
-
然后执行以下命令安装 Redis。
sudo apt install redis-server
-
现在执行以下命令检查 Redis 是否运行且可达:
redis-cli ping
redis-cli
是 Redis 功能的 CLI。 您应该看到来自 Terminal 的pong
响应。 就是这样。 Redis 现在已经安装在你的 Linux 机器上了。
所以,这就是在 Windows、macOS 和 Linux 机器上安装 Redis 服务器。 现在让我们在 ASP 中使用 Redis.NET Core 5。
在 ASP 中实现 Redis.NET Core
所以,让我们使用我们刚刚安装在机器上的 Redis 通过将其与我们现有的 ASP 集成.NET Core 5 解决方案。 以下是步骤:
-
Go to the
Travel.Application
project and install these NuGet packages. The following NuGet package is a distributed cache implementation of theMicrosoft.Extensions.Caching.StackExchangeRedis
namespace using Redis:Microsoft.Extensions.Caching.StackExchangeRedis
下面的 NuGet 包帮助我们检索
appsettings.json
中的配置:Microsoft.Extensions.Configuration
下面的 NuGet 包是。net 的 JSON 框架:
Newtonsoft.Json
-
Next, we update the
DependencyInjection.cs
file of theTravel.Application
project with the following code:``` namespace Travel.Application { public static class DependencyInjection { public static IServiceCollection AddApplication(this IServiceCollection services, IConfiguration config) { services.AddAutoMapper(Assembly.GetExecutingAssembly()); services.AddValidatorsFromAssembly(Assembly. GetExecutingAssembly()); services.AddMediatR(Assembly.GetExecutingAssembly()); services.AddStackExchangeRedisCache(options => { options.Configuration = config.GetConnectionString("RedisConnection");
var assemblyName = Assembly. GetExecutingAssembly().GetName(); options.InstanceName = assemblyName. Name; }); … return services; } }
} ```
前面的
Travel.Application
的依赖注入实现现在需要一个IConfiguration
参数。 我们将 Redis 分布式缓存服务添加到依赖注入容器中。 连接字符串的名称是RedisConnection
,我们将在下一步中对其进行设置。 -
接下来是转到
Travel.WebApi
项目并使用以下代码更新appsettings.json
:{ "AuthSettings": { "Secret": "ReplaceThsWithYour0wnSecretKeyAnd StoreItInAzureKeyVault!" }, "ConnectionStrings": { "DefaultConnection": "Data Source= TravelTourDatabase.sqlite3", "RedisConnection": "localhost:6379" }, "Logging": { … }, "MailSettings": { … }, "AllowedHosts": "*" }
-
We are adding connection strings for Redis and SQLite3 in this code. Consequently, we are also going to update
DependencyInjection.cs
ofTravel.Data
. So, let's update that with the following code:namespace Travel.Data { public static class DependencyInjection { public static IServiceCollection AddInfrastructureData(this IServiceCollection services, IConfiguration config) { services.AddDbContext<ApplicationDbContext>(options => options .UseSqlite(config.GetConnectionString("DefaultConnecti on"))); … } } }
Travel.Data
的依赖注入文件现在在appsettings.json
中定义了DefaultConnection
配置。 -
Another thing to do here is to update the
Startup.cs
file ofTravel.WebApi
. Go to that file and update it with the following code:public void ConfigureServices(IServiceCollection services) { services.AddApplication(Configuration); … services.AddHttpContextAccessor(); services.AddControllers(); … }
我们现在在
AddApplication
扩展方法中传递IConfiguration Configuration
。 这样,Travel.Application
可以访问appsettings.json
中的RedisConnection
。 -
Now let's use Redis to cache the response of the
localhost:5001/api/v1.0/TourLists
endpoint to its consumers sending aGET
request. To do this, we will update the handler ofapi/v1.0/TourLists
for theGET
request, which isGetToursQuery
.GetToursQuery
可在Travel.Application/TourLists/Queries/GetTours/GetTours/GetToursQuery.cs
中找到。 用以下代码更新GetToursQuery.cs
:… using Microsoft.Extensions.Caching.Distributed; using Newtonsoft.Json; … namespace Travel.Application.TourLists.Queries.GetTours { public class GetToursQuery : IRequest<ToursVm> { } public class GetToursQueryHandler : IRequestHandler<GetToursQuery, ToursVm> { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IDistributedCache _distributedCache; public GetToursQueryHandler( IApplicationDbContext context, IMapper mapper, IDistributedCache distributedCache) { _context = context; _mapper = mapper; _distributedCache = distributedCache; } public async Task<ToursVm> Handle( GetToursQuery request, CancellationToken cancellationToken) { … } } }
我们将
Microsoft.Extensions.Caching.Distributed
命名空间中的IDistributedCache
注入GetToursQueryHandler
的构造函数中。 我们将在Handle
方法的逻辑中使用distributedCache
,出于可读性的考虑,我将其截断。下面的代码是
Handle
方法更新后的业务逻辑:public async Task<ToursVm> Handle(GetToursQuery request, CancellationToken cancellationToken) { const string cacheKey = "GetTours"; ToursVm tourLists; string serializedTourList; var redisTourLists = await _distributedCache.GetAsync(cacheKey, cancellationToken); if (redisTourLists == null) { tourLists = new ToursVm { Lists = await _context.TourLists .ProjectTo<TourListDto>(_mapper. ConfigurationProvider) .OrderBy(t => t.City). ToListAsync( cancellationToken) }; serializedTourList = JsonConvert.SerializeObject(tourLists); redisTourLists = Encoding.UTF8.GetBytes(serializedTourList); var options = new DistributedCacheEntryOptions() .SetAbsoluteExpiration(DateTime.Now.AddMinutes(5)) .SetSlidingExpiration(TimeSpan.FromMinutes(1)); await _distributedCache.SetAsync( cacheKey,redisTourLists, options, cancellationToken); return tourLists; } serializedTourList = Encoding.UTF8.GetString( redisTourLists); tourLists = JsonConvert .DeserializeObject<ToursVm>(serializedTourList); return tourLists; }
前面的代码块是
GetToursQuery
处理程序的更新逻辑。 我们有"GetTours"
作为cacheKey
,我们将使用它从缓存中检索数据并从缓存中保存数据。cacheKey
将用于搜索特定缓存时的查找。我们还通过
_distributedCache.GetAsync
检查是否存在现有缓存。 如果没有数据,则序列化tourLists
对象并将其保存在缓存_distributedCache.SetAsync
中,然后返回tourLists
。 我们缓存的数据在 Redis,但我们把过期。SetAbsoluteExpiration
设置绝对过期时间,而SetSlidingExpiration
设置条目可以不活动多长时间。如果有数据,则返回一个反序列化的
tourLists
。现在,在我们继续 Vue.js 下一章,第 11 章,Vue.js 基本面 Todo 应用【显示】,
Startup.cs
让我们清理文件,因为它开始变得混乱。我们要做的是将 Swagger 配置移动到它的目录和文件中,然后安排所有服务并删除所有不必要的
using
语句。 -
So, go to
Travel.WebApi
and create a folder namedExtensions
in theroot
directory of the project. Create two C# files namedAppExtension.cs
andServices.Extensions.cs
. We are moving the Swagger code fromStartup.cs
to these two files like so:// AppExtension.cs
… namespace Travel.WebApi.Extensions { public static class AppExtensions { public static void UseSwaggerExtension(this IApplicationBuilder app, IApiVersionDescriptionProvider provider) { app.UseSwagger(); app.UseSwaggerUI(c => { ... }); } } }
这里,我们将两个中间件从
Configure
方法,即app.UserSwagger()
和app.UseSwaggerUI()
迁移到AppExtension.cs
文件中。// ServicesExtensions.cs
… namespace Travel.WebApi.Extensions { public static class ServicesExtensions { public static void AddApiVersioningExtension( this IServiceCollection services) { services.AddApiVersioning(config => { ... }); } public static void AddVersionedApiExplorerExtension(this IServiceCollection services) { services.AddVersionedApiExplorer(options => { ... }); } public static void AddSwaggerGenExtension(this IServiceCollection services) { services.AddSwaggerGen(c => { ... }); } } }
在这里,我们将
ConfigureServices
方法中的services.AddApiVersion()
、services.AddVersionedApiExplorer()
和services.AddSwaggerGen()
服务迁移到ServicesExtensions.cs
。 -
After moving the code to the
Extensions
directory, let's refactorStartup.cs
by calling theextension
methods that we created like so:public void ConfigureServices(IServiceCollection services) { services.AddApplication(Configuration); services.AddInfrastructureData(Configuration); services.AddInfrastructureShared(Configuration); services.AddInfrastructureIdentity(Configuration); services.AddHttpContextAccessor(); services.AddControllers(); services.AddApiVersioningExtension(); services.AddVersionedApiExplorerExtension(); services.AddSwaggerGenExtension(); services.AddTransient<IConfigureOptions<SwaggerGenOpti ons>, ConfigureSwaggerOptions>(); }
现在,让我们看看应用的中间件:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseSwaggerExtension(provider); } app.UseHttpsRedirection(); app.UseRouting(); app.UseMiddleware<JwtMiddleware>(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
上述代码为
Startup.cs
中间件的重构块。 中间件现在比以前更干净了。同样,删除您将在
Startup.cs
文件中找到的未使用的using
语句。让我们运行应用,看看 Redis 是否正常工作:
-
Send a
GET
request to/api/v1.0/TourLists
using Postman. Don't forget to include your JWT. The following screenshot shows the response time of the first request to the ASP.NET Core 5 application, which is more than 2 seconds:图 10.4 -没有 Redis 缓存的 API 响应
-
Let's send the same request to the same API to see whether the response time will be shorter:
图 10.5 - Redis 缓存的 API 响应
前面的屏幕截图显示了第二个
GET
请求的较短的响应时间,33 毫秒,这是由于在对相同 API 的第一个GET
请求期间存储了缓存。 -
要在中查看缓存,你可以使用 Redis 管理器工具。 这是一个免费的复述,马钎子工具,你可以下载并安装,https://www.electronjs.org/apps/anotherredisdesktopmanager,【T7 和付费版本发现 https://rdm.dev/】【显示】。 RDM 是为 Windows 和 macOS 用户提供的付费应用,但不适用于 Linux 用户。
-
After running the Redis manager tool, send a new request to the
/api/v1.0/TourLists
API and check your Redis manager tool.让我们检查一下 Windows 10 v20H2、macOS Pro Big Sur 和 Ubuntu v20.10 Groovy Gorilla 中的缓存。 这些操作系统是撰写本书时的最新版本。
下面的截图显示
AnotherRedisDeskTopManager
在 Windows 上运行:
图 10.6 - Windows 上的另一个 Redis 桌面管理器
下面的截图显示了在 macOS 上运行的AnotherRedisDeskTopManager
:
图 10.7 -另一个在 macOS 上的 Redis 桌面管理器
下面的截图显示了在 Ubuntu 上运行的 Redis GUI:
图 10.8 - Ubuntu 上的 Redis GUI
如果你不喜欢任何仪表盘或 GUI 的 Redis,你也可以使用一个 CLI 命令来调试或监控每一个命令处理你的 Redis 服务器。 运行命令开启监控:
redis-cli monitor
下面的屏幕截图显示了在运行redis-cli monitor
后,您的请求在命令行中的样子:
图 10.9 - redis-cli 监视器
代码更新
接下来,我们更新应用中的一些代码,并更改命名约定,这对于前端准备来说非常简单。
以下是包含需要更新的项目、目录和文件的路径。 所以,去这一章的 GitHub repo,写下你的文件中缺少的东西,或者你可以从 GitHub 复制代码并粘贴到你的代码中。
同样,这些是命名约定、新属性和类中的简单更改。
Travel.Domain/Entities/TourList.cs
:
public TourList()
{
TourPackages = new List<TourPackage>();
}
public IList<TourPackage> TourPackages { get; set; }
前面的代码是更新TourList
类中的Tours
。
Travel.Domain/Entities/User.cs
:
public string Email { get; set; }
前面的代码正在更新User
类中的Username
。
Travel.Application/Dtos/Tour/TourPackageDto.cs
:
…
public string WhatToExpect { get; set; }
public float Price { get; set; }
public string MapLocation { get; set; }
public void Mapping(Profile profile)
{
profile.CreateMap<TourPackage,
TourPackageDto>()
.ForMember(tpDto =>
tpDto.Currency, opt =>
opt.MapFrom(tp =>
(int)tp.Currency));
}
前面的代码正在更新TourPackageDto
c 类。
Travel.Application/Dtos/Tour/TourListDto.cs
:
public TourListDto()
{
TourPackages = new List<TourPackageDto>();
}
public IList<TourPackageDto> TourPackages { get; set; }
public string Country { get; set; }
前面的代码正在更新TourListDto
类。
Travel.Application/Dtos/User/AuthenticateRequest.cs
:
public string Email { get; set; }
前面的代码正在更新AuthenticateRequest
类中的Username
。
Travel.Application/Dtos/User/AuthenticateResponse.cs
:
public string Email { get; set; }
…
Email = user.Email;
前面的代码正在AuthenticateResponse
中更新Username
。
Travel.Application/TourLists/Commands/CreateTourList/CreateTourListCommand.cs
:
var entity = new TourList { City = request.City, Country =
request.Country, About = request.About };
前面的代码是在CreateTourListCommand
中添加properties
。
Travel.Application/TourLists/Commands/UpdateTourList/UpdateTourListCommand.cs
:
entity.Country = request.Country;
entity.About = request.About;
前面的代码是在UpdateTourListCommand
中添加属性。
在TourPackages
目录中创建一个新的文件夹,并将其命名为Queries
。 在查询中,创建两个新的 c#文件,并将其命名为GetTourPackagesQuery.cs
和GetTourPackagesValidator.cs.
Travel/Application/TourPackages/Queries/GetTourPackagesQueryValidator.cs
:
using System.Collections.Generic;
… // for brevity, please see the code in the Github
using Travel.Application.Dtos.Tour;
namespace Travel.Application.TourPackages.Queries
{
public class GetTourPackagesQuery : IRequest
<List<TourPackageDto>>
{
public int ListId { get; set; }
}
public class GetTourPackagesQueryHandler :
IRequestHandler<GetTourPackagesQuery, List<
TourPackageDto>>
{
private readonly IApplicationDbContext _context;
private readonly IMapper _mapper;
public GetTourPackagesQueryHandler(
IApplicationDbContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public Task<List<TourPackageDto>>
Handle(GetTourPackagesQuery request,
CancellationToken cancellationToken)
{
var tourPackages = _context.TourPackages
.Where(tp => tp.ListId == request.ListId)
.OrderBy(tp => tp.Name)
.ProjectTo<TourPackageDto>(_mapper.
ConfigurationProvider)
.ToListAsync(cancellationToken);
return tourPackages;
}
}
}
在前面的代码中没有 ing new。 新文件简单地添加了一个获得旅行团的查询。
Travel.Application/TourPackages/Queries/GetTourPackagesQueryValidator.cs
:
using FluentValidation;
namespace Travel.Application.TourPackages.Queries
{
public class GetTourPackagesQueryValidator :
AbstractValidator<GetTourPackagesQuery>
{
public GetTourPackagesQueryValidator()
{
RuleFor(x => x.ListId)
.NotNull()
.NotEmpty().WithMessage("ListId is
required.");
}
}
}
前面的代码行在查询旅行包之前添加了一个新的验证器。
Travel.Identity/Services/UserService.cs
:
Email = "yoursuperhero@gmail.com",
var user = _users.SingleOrDefault(u => u.Email ==
model.Email &&
…
u.Password == model.Password);
…
Subject = new ClaimsIdentity(new[] { new Claim("sub",
user.Id.ToString()), new Claim("email", user.Email) }),
前面的代码正在更新UserService
类。
Travel.Identity/Helpers/JwtMiddleware.cs
:
var userId = int.Parse(jwtToken.Claims.First(c => c.Type ==
"sub").Value);
前面的代码是更新JwtMiddleware
类的AttachUserToContext
方法。
Travel.WebApi/Controllers/v1/TourPackagesController.cs
:
[HttpGet]
public async Task<ActionResult<List<TourPackageDto>>>
GetTourPackages([FromQuery] GetTourPackagesQuery query)
{
return await Mediator.Send(query);
}
上述代码是TourPackagesController
的一个新的Action
方法。
现在,在您的存储库中更新了 code 之后,是时候挑战自己了。
运动/练习时间:
为了加强您在这里的学习,并在继续到前端部分之前,我希望您创建一个 ASP.NET Core 5 应用。 应用应该使用你在这本书中学到的所有东西,比如干净的架构、CQRS、API 版本控制、OpenAPI 和分布式缓存,无需认证或使用认证,或者使用像 Auth0 这样的身份作为服务来节省你的时间。 我现在能想到的一个应用是电子游戏的在线商店。 实体可以是Developer
、Game
、GameReviews
、Genre
、Publisher
等。 这个练习很简单,你可以在一周内完成。 我知道你能做到。 好运!
好的,让我们总结一下你在这一章所学到的东西。
总结
你终于读完了这一章,你学到了很多东西。 您已经了解到内存缓存比分布式缓存更快,因为它更靠近服务器。 但是,它不适用于同一服务器的多个实例。
您已经了解了分布式缓存解决了多个实例中的内存缓存问题,因为它为所有服务器实例提供了缓存数据的单一真实来源。
你已经学习了如何安装和运行 Redis 在 pc, macOS 和 Linux 机器,以及如何整合 Redis 到一个 ASP.NET Core Web API 来提高应用的性能,给最终用户带来更好的用户体验。
在下一章中,您将使用 Vue.js 3 构建您的第一个单页面应用。
版权属于:月萌API www.moonapi.com,转载请注明出处