天气API推荐:精准获取气象数据的首选
在.NET API中使用内存中缓存时,性能提高了90%!
让我们谈谈缓存!我们将介绍缓存的基础知识,ASP.NET核心应用程序中的内存缓存等。我们将构建一个简单的.NET 8 Web API,它可以帮助演示如何从应用程序的内存缓存中设置和获取缓存条目。缓存可以显著提高API的性能,如果有效地实现,还可以帮助节省大量成本。我们开始吧!
什么是缓存?
缓存是一种将频繁访问/使用的数据存储在临时存储中的技术,以便将来对这些数据集的请求可以更快地服务于客户端。
缓存涉及获取最频繁访问和最不频繁修改的数据,并将其复制到临时存储器。这使得未来的客户端请求能够更快地访问这些数据,通过减少对原始数据源的不必要和频繁的请求(这可能很耗时,甚至可能涉及一些成本)来显著提高应用程序性能。
这里有一个缓存工作原理的简化示例:假设,当客户端#1请求某些数据时,从数据存储中获取它需要大约5秒。一旦数据被提取,它也被复制到临时存储器。现在,当客户端#2请求相同的数据时,可以在不到1-2秒的时间内从该高速缓存中检索该数据。这减少了等待时间并提高了应用程序的整体效率。此外,这一次,我们的应用程序不是从数据存储中获取数据,而是从该高速缓存内存中获取所需的数据。
你可能想知道,如果数据发生变化会发生什么?我们还会提供过时的回复吗?答案是否定的。有多种策略可以刷新该高速缓存并设置过期时间,以确保数据保持最新。我们将在本文后面详细讨论这些技术。
重要的是要注意,应用程序不应该仅仅依赖于缓存的数据,它永远不应该是应用程序的真实来源。只有在缓存数据可用并且是最新的情况下,它们才应该使用缓存数据。如果该高速缓存数据已过期或不可用,则应用程序应从原始源请求数据。这确保了数据的准确性和可靠性。
ASP.NET Core中的缓存
ASP.NET Core为各种类型的缓存提供了出色的开箱即用支持:
- 内存缓存:数据缓存在服务器的内存中。
- 分布式缓存:数据存储在应用程序外部的源中,如Redis缓存。
- 混合缓存:这是对分布式缓存的改进,将从.NET 9引入。
下面是缓存工作原理的演示。
请注意,这种特定的机制被称为该高速缓存旁路模式,它通常用于读重应用程序。
在这篇文章中,我们将详细讨论内存缓存
。
什么是ASP.NET Core中的内存缓存?
在ASP.NET Core中,内存缓存允许您将数据存储在应用程序的内存中。这意味着数据被缓存在服务器的实例上,这通过减少重复获取相同数据的需求来显著提高应用程序的性能。内存缓存是在应用程序中实现缓存的最简单、最有效的方法之一。
内存缓存的优点和缺点
优点
- 更快:它避免了网络通信,使其比其他形式的分布式缓存更快。
- 高度可靠:数据直接存储在服务器内存中,确保快速访问。
- 最适合中小型应用程序:它为较小的应用程序提供了有效的性能改进。
缺点
- 资源消耗:如果配置不正确,它可能会消耗大量的服务器资源。
- 可伸缩性问题:随着应用程序的扩展和缓存时间的延长,维护服务器的成本可能会变得很高。
- 云部署挑战:在云部署中维护一致的缓存可能很困难。
- 不适用于具有多个副本的微服务:内存缓存可能不适用于运行多个副本的微服务架构,因为每个副本都有自己的缓存,可能导致缓存数据的不一致。
入门
在ASP.NETCore中设置缓存非常简单。只需几行代码,就可以显著提高应用程序的响应能力,通常提高50-75%或更多。
让我们创建一个新的ASP.NET Core 8 Web API。我将使用Visual Studio 2022社区版进行此演示
要启用内存中缓存,首先需要在应用程序的服务容器中注册缓存服务。然后我们将看到如何使用依赖注入。
导航到Program.cs
文件,并添加以下内容。
builder.Services.AddMemoryCache();
这将向应用程序的IServiceCollection
添加一个非分布式的内存中实现。
就是这样。您的应用程序现在支持内存缓存托管。现在,为了更好地理解缓存是如何工作的,我们将创建几个CRUD端点。
我将使用EF核心代码优先方法,并与本地计算机上托管的PostgreSQL数据库进行交互。
让我们先安装所需的EF Core包。
Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.Tools
Install-Package Npgsql.EntityFrameworkCore.PostgreSQL
如前所述,我们将构建一个ASP.NET Core 8 Web API,它具有端点,
- 获取所有产品:在这里,产品列表将被缓存。
- 获取单个产品:这也将被缓存。
- 添加产品:在此操作后,所有产品的缓存列表将被删除/无效。
建立产品模型
像往常一样,我们将有一个相当简单的产品模型。
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; } = default!;
public string Description { get; set; } = default!;
public decimal Price { get; set; }
// Parameterless constructor for EF Core
private Product() { }
public Product(string name, string description, decimal price)
{
Id = Guid.NewGuid();
Name = name;
Description = description;
Price = price;
}
}
我还添加了一个ProductCreationDTO,我们将在ProductService
类中使用它。
public record ProductCreationDto(string Name, string Description, decimal Price);
我不打算一步一步地介绍如何设置DB Context类以及如何生成和应用迁移。您可以在本文末尾找到完整的源代码。
但是,下面是我用来连接到本地PGSQL实例的连接字符串。
"ConnectionStrings": {"dotnetSeries": "Host=localhost;Database=dotnetSeries;Username=postgres;Password=admin;Include Error Detail=true"}
我还使用MockarooWeb应用程序为产品生成了1000条假记录。我将此数据生成为SQL插入脚本,并在迁移的数据库中运行它。因此,我们的“产品”表中有1,000条样本记录可用。SQL脚本包含在解决方案的SQL文件
夹中,以防您也需要它。
产品服务-使用IMemoryCache
这是产品服务类,它为我们做了所有繁重的工作。让我们一个方法一个方法地检查代码。
请注意,我已经将AppDbContext
、IMemoryCache
和ILogger<ProductService>
实例注入到产品服务类的主构造函数中,以便我们可以在整个类中访问它们。
public class ProductService(AppDbContext context, IMemoryCache cache, ILogger<ProductService> logger) : IProductService
{
public async Task Add(ProductCreationDto request)
{
var product = new Product(request.Name, request.Description, request.Price);
await context.Products.AddAsync(product);
await context.SaveChangesAsync();
// invalidate cache for products, as new product is added
var cacheKey = "products";
logger.LogInformation("invalidating cache for key: {CacheKey} from cache.", cacheKey);
cache.Remove(cacheKey);
}
public async Task<Product> Get(Guid id)
{
var cacheKey = $"product:{id}";
logger.LogInformation("fetching data for key: {CacheKey} from cache.", cacheKey);
if (!cache.TryGetValue(cacheKey, out Product? product))
{
logger.LogInformation("cache miss. fetching data for key: {CacheKey} from database.", cacheKey);
product = await context.Products.FindAsync(id);
var cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(30))
.SetAbsoluteExpiration(TimeSpan.FromSeconds(300))
.SetPriority(CacheItemPriority.Normal);
logger.LogInformation("setting data for key: {CacheKey} to cache.", cacheKey);
cache.Set(cacheKey, product, cacheOptions);
}
else
{
logger.LogInformation("cache hit for key: {CacheKey}.", cacheKey);
}
return product;
}
public async Task<List<Product>> GetAll()
{
var cacheKey = "products";
logger.LogInformation("fetching data for key: {CacheKey} from cache.", cacheKey);
if (!cache.TryGetValue(cacheKey, out List<Product>? products))
{
logger.LogInformation("cache miss. fetching data for key: {CacheKey} from database.", cacheKey);
products = await context.Products.ToListAsync();
var cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(30))
.SetAbsoluteExpiration(TimeSpan.FromSeconds(300))
.SetPriority(CacheItemPriority.NeverRemove)
.SetSize(2048);
logger.LogInformation("setting data for key: {CacheKey} to cache.", cacheKey);
cache.Set(cacheKey, products, cacheOptions);
}
else
{
logger.LogInformation("cache hit for key: {CacheKey}.", cacheKey);
}
return products;
}
}
获取所有产品
从第36行到第57行,我们有一个从数据库/缓存返回所有产品列表的方法。
我们首先设置一个缓存键,在当前情况下是products
。此键将用作标识符,用于将数据存储在该高速缓存中。正如你所知道的,缓存在技术上是存储在数据结构中的键值对,就像字典一样。
接下来,在第40行中,我们首先尝试检查该高速缓存中是否有针对关键产品的
可用数据。如果存在某些数据,则将其提取到List<Product>
中。否则,控件进入if语句,我们将从数据库中获取数据。请注意,只有当缓存未命中时才会出现这种情况,或者换句话说,只有当数据不存在于我们的缓存存储中时才会出现这种情况。
一旦我们从数据库获得响应,我们就根据该高速缓存键将该数据设置到我们的缓存存储中,并将其返回给客户机。
Get(Guid id)
方法的实现也几乎类似,我们在其中传递Product ID。然而,主要区别是,这一次,我们有一个更动态的缓存,即product:{productId}
,基于请求的Product ID。
ASP.NET Core中的缓存条目选项
配置该高速缓存选项允许您根据需要自定义缓存行为。
var cacheOptions = new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(20)).SetSlidingExpiration(TimeSpan.FromMinutes(2)).SetPriority(CacheItemPriority.NeverRemove);
MemoryCacheEntryOptions
-这个类用于定义相关缓存技术的关键属性。我们将创建这个类的一个实例,并将其传递给IMemoryCache
实例。但在此之前,让我们了解MemoryCacheEntryOptions
的属性。
在ASP.NET Core中使用缓存时,可以配置几个设置来优化性能和资源管理。以下是一些关键设置及其用途:
优先级(SetPriority()
)
- 说明:设置保留该高速缓存条目的优先级。这决定了当该高速缓存超过内存限制时删除条目的可能性。
- 选项:
正常
(默认)-
高
-
低
NeverRemove
Size(SetSize()
)
- 说明:指定该高速缓存条目的大小。这有助于防止该高速缓存消耗过多的服务器资源。
SlidingList(SetSlidingList()
)
- 说明:设置一个时间间隔,在此时间间隔内,如果不访问该高速缓存条目,则该条目将过期。在本例中,2分钟滑动过期意味着如果在2分钟内没有人访问该该高速缓存条目,则该条目将被删除。
- 用途:
cacheEntry.SetSlidingExpiration(TimeSpan.FromMinutes(2));
绝对值(SetAbsoluteArray()
)
- 描述:定义一个固定的时间,在此时间之后,该高速缓存条目将过期,而不管访问它的频率。这可以防止该高速缓存无限期地提供过时的数据。
- 最佳实践:始终同时使用滑动到期和绝对到期。请确保绝对到期时间长于滑动到期时间,以避免冲突。
- 用途:
cacheEntry.SetAbsoluteExpiration(TimeSpan.FromMinutes(20));
关于ASP.NET Core中的内存缓存,在构建应用程序时,必须仔细考虑Priority、Size、Sliding Size和Absolute Size属性。
添加产品-缓存失效
缓存并不像听起来那么简单。虽然它可以通过减少重复获取数据的需要来显著提高应用程序性能,但有效管理该高速缓存涉及确保缓存的数据保持准确和最新。这就是缓存失效发挥作用的地方。
缓存失效是至关重要的,因为它可以确保陈旧或过期的数据不会在该高速缓存中持久存在,从而导致应用程序中的不一致和潜在的错误行为。
这里有一个场景。如果用户创建了一个新产品,然后试图获取所有产品的列表,该怎么办?我们的应用程序很可能会提供产品创建之前的陈旧数据。当基础数据发生更改时,必须更新该高速缓存以反映这些更改。
缓存失效策略
基于时间的失效
- 滑动缓存:如果该高速缓存条目未被访问,则在指定时间后该条目将过期。这对于访问频率较低的数据很有用。
- 绝对缓存:该高速缓存条目在固定时间后过期,与访问频率无关。这样可以确保定期刷新数据。
基于依赖性的失效
- 数据失效:当缓存条目所依赖的数据发生变化时,缓存条目将失效。这可以使用基于事件的机制或更改通知来实现。
- 父子关系:在缓存数据是分层的情况下,对父数据的更改可能会触发子数据的无效。
手动失效
- 显式删除:当满足某些条件或执行特定操作时,开发人员可以显式删除缓存条目。例如,在更新数据库记录之后,可以移除或更新对应的高速缓存条目。
- 标记:将标记添加到缓存条目允许批量无效。例如,所有带有特定标签的条目可以立即无效。
对于我们的演示,我们将使用手动缓存无效。每当添加新产品时,我们调用cache.Remove(cacheKey);
确保产品列表从该高速缓存中删除。
您可以通过更新该高速缓存来进一步增强此机制,而不是完全清除它。例如,当创建新产品时,您可以从该高速缓存中获取产品列表,将新创建的产品追加到列表中,并再次将更新后的列表重新添加到该高速缓存内存中。这可能会增加往返于该高速缓存内存的次数,但根据您的系统设计,这也可能是高效的。
您还可以将整个缓存失效过程作为后台任务运行,这样应用程序就不必等待该高速缓存调用完成才发送响应
最小API端点
现在我们已经实现了所需的服务方法,让我们将产品服务连接到.NET 8 Web API中的实际API端点。打开Program.cs
,并添加以下最小API端点。
app.MapGet("/products", async (IProductService service) =>
{
var products = await service.GetAll();
return Results.Ok(products);
});
app.MapGet("/products/{id:guid}", async (Guid id, IProductService service) =>
{
var product = await service.Get(id);
return Results.Ok(product);
});
app.MapPost("/products", async (ProductCreationDto product, IProductService service) =>
{
await service.Add(product);
return Results.Created();
});
您可能还必须将IProductService
服务注册到具有Transient作用域的依赖注入(DI)容器中。
builder.Services.AddTransient<IProductService, ProductService>();
在ASP.NET Core中测试缓存
让我们构建并运行应用程序。我将使用Postman来测试我们的实现。
首先,让我们访问GetAll
端点。由于该高速缓存中没有数据,因此我们希望从数据库中获取数据。
如您所见,返回1000个产品的列表的响应时间接近800 ms。理想情况下,产品列表应该被缓存,如果我们再次访问端点,我们希望从该高速缓存中获取它。
我们再来一次。
现在,响应时间大大降低,降至40毫秒以下!
日志和我们预期的一样。
要记住的要点
下面是实现缓存时需要考虑的一些要点。
- 您的应用程序永远不应该依赖于缓存数据,因为它很可能在任何给定时间都不可用。传统上,它应该取决于您的实际数据源。缓存只是一种增强功能,只有在可用/有效时才能使用。
- 尝试限制内存中该高速缓存的增长。这是至关重要的,因为如果配置不正确,缓存可能会占用您的服务器资源。可以使用Size属性限制用于条目的该高速缓存。
- 使用AbsoluteReader/SlidingReader使您的应用程序更快、更智能。它还有助于限制缓存内存的使用。
后台缓存更新
此外,作为一项改进,您可以使用后台作业定期更新该高速缓存。如果“绝对缓存”设置为5分钟,则可以每6分钟运行一次重复作业,以将该高速缓存条目更新为其最新版本。您可以使用Hangfire在ASP.NET核心应用程序中实现相同的功能。
让我们在这里结束这篇文章。
在下一篇文章中,我们将讨论更高级的缓存概念,如分布式缓存,设置Redis,Redis缓存,PostBack调用等等。
总结
在这篇详细的文章中,我们探讨了缓存的各个方面,包括基础知识、内存中缓存以及如何在ASP.NET Core中实现内存中缓存。
文章转自微信公众号@科控物联