所有文章 > API使用场景 > 在.NET API中使用内存中缓存时,性能提高了90%!
在.NET API中使用内存中缓存时,性能提高了90%!

在.NET API中使用内存中缓存时,性能提高了90%!

让我们谈谈缓存!我们将介绍缓存的基础知识,ASP.NET核心应用程序中的内存缓存等。我们将构建一个简单的.NET 8 Web API,它可以帮助演示如何从应用程序的内存缓存中设置和获取缓存条目。缓存可以显著提高API的性能,如果有效地实现,还可以帮助节省大量成本。我们开始吧!

什么是缓存?

缓存是一种将频繁访问/使用的数据存储在临时存储中的技术,以便将来对这些数据集的请求可以更快地服务于客户端。

缓存涉及获取最频繁访问和最不频繁修改的数据,并将其复制到临时存储器。这使得未来的客户端请求能够更快地访问这些数据,通过减少对原始数据源的不必要和频繁的请求(这可能很耗时,甚至可能涉及一些成本)来显著提高应用程序性能。

这里有一个缓存工作原理的简化示例:假设,当客户端#1请求某些数据时,从数据存储中获取它需要大约5秒。一旦数据被提取,它也被复制到临时存储器。现在,当客户端#2请求相同的数据时,可以在不到1-2秒的时间内从该高速缓存中检索该数据。这减少了等待时间并提高了应用程序的整体效率。此外,这一次,我们的应用程序不是从数据存储中获取数据,而是从该高速缓存内存中获取所需的数据。

你可能想知道,如果数据发生变化会发生什么?我们还会提供过时的回复吗?答案是否定的。有多种策略可以刷新该高速缓存并设置过期时间,以确保数据保持最新。我们将在本文后面详细讨论这些技术。

重要的是要注意,应用程序不应该仅仅依赖于缓存的数据,它永远不应该是应用程序的真实来源。只有在缓存数据可用并且是最新的情况下,它们才应该使用缓存数据。如果该高速缓存数据已过期或不可用,则应用程序应从原始源请求数据。这确保了数据的准确性和可靠性。

ASP.NET Core中的缓存

ASP.NET Core为各种类型的缓存提供了出色的开箱即用支持:

  1. 内存缓存:数据缓存在服务器的内存中。
  2. 分布式缓存:数据存储在应用程序外部的源中,如Redis缓存。
  3. 混合缓存:这是对分布式缓存的改进,将从.NET 9引入。

下面是缓存工作原理的演示。


请注意,这种特定的机制被称为该高速缓存旁路模式,它通常用于读重应用程序。

在这篇文章中,我们将详细讨论内存缓存

什么是ASP.NET Core中的内存缓存?

在ASP.NET Core中,内存缓存允许您将数据存储在应用程序的内存中。这意味着数据被缓存在服务器的实例上,这通过减少重复获取相同数据的需求来显著提高应用程序的性能。内存缓存是在应用程序中实现缓存的最简单、最有效的方法之一。

内存缓存的优点和缺点

 优点

  1. 更快:它避免了网络通信,使其比其他形式的分布式缓存更快。
  2. 高度可靠:数据直接存储在服务器内存中,确保快速访问。
  3. 最适合中小型应用程序:它为较小的应用程序提供了有效的性能改进。

 缺点

  1. 资源消耗:如果配置不正确,它可能会消耗大量的服务器资源。
  2. 可伸缩性问题:随着应用程序的扩展和缓存时间的延长,维护服务器的成本可能会变得很高。
  3. 云部署挑战:在云部署中维护一致的缓存可能很困难。
  4. 不适用于具有多个副本的微服务:内存缓存可能不适用于运行多个副本的微服务架构,因为每个副本都有自己的缓存,可能导致缓存数据的不一致。

入门

在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,它具有端点,

  1. 获取所有产品:在这里,产品列表将被缓存。
  2. 获取单个产品:这也将被缓存。
  3. 添加产品:在此操作后,所有产品的缓存列表将被删除/无效。

建立产品模型

像往常一样,我们将有一个相当简单的产品模型。

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

这是产品服务类,它为我们做了所有繁重的工作。让我们一个方法一个方法地检查代码。

请注意,我已经将AppDbContextIMemoryCacheILogger<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毫秒以下!

日志和我们预期的一样。

要记住的要点

下面是实现缓存时需要考虑的一些要点。

  1. 您的应用程序永远不应该依赖于缓存数据,因为它很可能在任何给定时间都不可用。传统上,它应该取决于您的实际数据源。缓存只是一种增强功能,只有在可用/有效时才能使用。
  2. 尝试限制内存中该高速缓存的增长。这是至关重要的,因为如果配置不正确,缓存可能会占用您的服务器资源。可以使用Size属性限制用于条目的该高速缓存。
  3. 使用AbsoluteReader/SlidingReader使您的应用程序更快、更智能。它还有助于限制缓存内存的使用。

后台缓存更新

此外,作为一项改进,您可以使用后台作业定期更新该高速缓存。如果“绝对缓存”设置为5分钟,则可以每6分钟运行一次重复作业,以将该高速缓存条目更新为其最新版本。您可以使用Hangfire在ASP.NET核心应用程序中实现相同的功能。

让我们在这里结束这篇文章。

在下一篇文章中,我们将讨论更高级的缓存概念,如分布式缓存,设置Redis,Redis缓存,PostBack调用等等。

总结

在这篇详细的文章中,我们探讨了缓存的各个方面,包括基础知识、内存中缓存以及如何在ASP.NET Core中实现内存中缓存。

文章转自微信公众号@科控物联

#你可能也喜欢这些API文章!