65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 IDistributed Cache 与 EF Core

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2024年7月10日

CPOL

16分钟阅读

viewsIcon

5703

downloadIcon

180

在本文中,我将详细介绍如何使用 IDistributed Cache 来提高 EF Core 查询的性能,涵盖本地缓存系统,如 Redis、NCache、SqlCache、MySqlCache、MongoDBCache 以及 API 输出缓存。

引言

本文档概述了如何配置各种(本地)缓存系统,并将它们集成到 Entity Framework Core 查询中,以提高吞吐量并减少不必要的数据库(select)调用。

目的

您的解决方案可能处于需要扩展或从内存缓存方法(例如 IMemoryCache)迁移到更健壮、生产级别的缓存系统的境地。这样的系统允许您在需要时向集群添加额外节点,或在服务器重启后保持数据完整性,或将缓存数据复制到多个位置。

范围

本文档的范围是快速传达缓存系统的配置步骤,但更重要的是如何在 .Net Core 中使用它们,并且不会损害 SOLID 原则,如果您将来切换缓存机制(您的服务仍然继承自 IDistributedCache)。

必备组件

为什么要在解决方案中引入分布式缓存机制

分布式缓存是一种将缓存维护为外部服务,供多个应用程序服务器使用的技术。这与内存缓存不同,内存缓存将数据缓存在应用程序服务器的内存中(导致在重启时数据会丢失)。分布式缓存可以极大地提高应用程序的性能和可伸缩性,当我们在多台服务器或云上托管应用程序时,这是一个绝佳的选择。

1. 提高性能

  • 降低延迟:将频繁访问的数据缓存在内存中,与从较慢的后端数据存储或数据库获取数据相比,显著减少了访问时间。
  • 快速数据检索:分布式缓存提供对缓存数据的快速访问,减少了响应服务所需的时间。

2. 可伸缩性

  • 水平扩展:通过向缓存集群添加更多节点,分布式缓存可以水平扩展。这使得缓存能够处理不断增长的负载,而不会出现显着的性能下降。
  • 负载均衡:通过将缓存数据分布到多个节点,负载被平均分配,防止任何单个节点成为瓶颈。

3. 高可用性和容错能力

  • 复制:Redis 和 NCache 等分布式缓存支持跨多个节点的数据复制,确保在节点发生故障时数据不会丢失。
  • 故障转移支持:在节点发生故障时,缓存可以自动故障转移到副本节点,确保持续可用。

4. 成本效益

  • 减少后端负载:通过将频繁的读取操作分载到缓存,减少了后端数据库和服务的负载,可能降低与数据库扩展和性能优化相关的成本。
  • 高效的资源利用:对于读取操作而言,内存通常比持久化存储更快且成本更低,为高读取场景提供了成本效益高的解决方案。

5. 灵活性和功能

  • 高级数据结构:Redis 等分布式缓存提供了多种数据结构,如字符串、哈希、列表、集合和有序集合,可用于高效地存储和检索不同类型的数据。
  • TTL 和过期策略:缓存支持生存时间(TTL)和过期策略,允许您自动使陈旧数据失效和删除。
  • 发布/订阅和其他功能:一些缓存,如 Redis,提供额外的功能,如发布/订阅消息、Lua 脚本和事务,为您的应用程序架构增加了更多灵活性。

6. 支持各种用例

  • 会话存储:将用户会话存储在分布式缓存中,确保会话数据可供多个应用程序实例访问,从而促进负载均衡和水平扩展。
  • 内容缓存:缓存频繁访问的静态内容,如图像、HTML 和配置数据,可以减少延迟和后端负载。
  • 数据库缓存:缓存数据库查询结果可以通过减少数据库读取次数来显着提高应用程序性能。
  • 分布式锁定:分布式缓存可用于分布式锁定机制,以在分布式环境中同步对共享资源的访问。

7. 一致性和数据完整性

  • 原子操作:Redis 等分布式缓存提供原子操作,确保即使多个客户端并发访问和修改缓存,数据完整性也能得到维护。
  • 驱逐策略:各种驱逐策略(例如 LRU、LFU)有助于管理缓存大小,并确保最相关的数据保留在内存中。

项目结构

下面是解决方案的布局,每个缓存系统有多个项目。这样我们就可以同时处理多个请求,其中一个服务器会发现一个键的缓存为空(缓存未命中),然后查询数据库并填充缓存。第二次服务器调用将会在缓存中找到数据(缓存命中),并直接返回,而无需访问数据库。

“响应输出缓存”示例只有一个项目,因为数据缓存在各自的服务器上(而不是在分布式缓存系统级别)。因此,第二次服务器 API 调用使用响应输出缓存时,不会知道第一次服务器的缓存。但我包含了它,因为我相信它是一个很好的机制,可以与分布式缓存系统结合使用。

我将“MongoDB 缓存系统”实现为一个纯缓存**集合**,以 SQL Server 数据库作为数据库查询的**真相来源**,并将(非 SQL)MongoDB 作为**缓存来源**。

缓存系统设置(本地)

Redis

以下 YouTube 视频 是在 Windows 上安装 Redis 的一个很好的指南。您可以从此 GitHub 站点 下载(msi)安装程序。

安装完成后,导航到安装文件夹,然后双击文件 *redis-server.exe* 启动 Redis。

要使用 GUI 工具监视和配置 Redis,请下载并安装 Redis Insights。然后搜索 Redis Insight。

在这里,您可以通过向(分布式缓存)集群添加更多节点来扩展您的缓存。

现在您可以查看/过滤 Redis 缓存本身中的数据。

NCache

要使用 NCache 的分布式功能,您需要下载 **Enterprise** 版本。填写适当的详细信息,然后下载。

安装 NCache 时,请勿选择 **Developer/QA** 选项 - 这是免费的,但您将无法使用分布式缓存功能 - 请选择 **Cache Server** 选项。

您将收到一封包含安装密钥的电子邮件,请在安装过程中使用此密钥。

安装完成后,您可以通过搜索 **NCache** 打开 **NCache Web Portal**: https://:8251/ClusteredCaches

如果您发现 NCache Web Portal 页面未加载,请确保 NCache 服务(**NCacheSvc**)正在运行。

下面是一个集群(分布式)缓存的示例。

注意:如果您想将缓存数据作为文本而不是二进制数据查看,请在“查看详细信息”链接中更改此选项,从 Binary 更改为 JSON 格式并保存更改。

注意:我发现 NuGet 包(*NCache.Microsoft.Extensions.Caching*)安装的 *config.ncconf* 文件与代码中的配置设置产生了冲突 - 因此我从项目中删除了 *config.ncconf* 文件。

注意:将 NCache 项目配置为目标 OS 架构为 **x86**,我认为这更多地与 NCache 的架构有关!

代码解释

IDistributedCache 接口

IDistributedCache 接口是 .NET 缓存基础设施的关键组成部分,旨在提供一个简单且一致的 API 来处理分布式缓存。该接口属于 Microsoft.Extensions.Caching.Distributed 命名空间,通常用于需要在多台服务器之间缓存数据,确保所有应用程序实例都能访问缓存数据的场景。 IDistributedCache 接口提供了以下方法来操作分布式缓存实现中的项。

  1. GetGetAsync:接受一个字符串键,并在缓存中找到时以 `byte[]` 数组的形式检索缓存项。
  2. SetSetAsync:使用字符串键将一个项(作为 `byte[]` 数组)添加到缓存中。
  3. RefreshRefreshAsync:根据键刷新缓存中的项,重置其滑动过期时间(如果有)。
  4. RemoveRemoveAsync:根据字符串键删除缓存项。

通用代码

解决方案将使用一个抽象类,该类有一个名为 `RetrieveEmployeeByNameAsync` 的自定义方法。为每个数据库上下文都创建了一个服务(为了简单起见)。

`RetrieveEmployeeByNameAsync` 方法实现的示例。每个服务都从抽象类(`DistributedServiceBase`)继承此方法。您会看到 `IDistributedCache` 被注入到抽象类中,但它将与不同的 DBContext 相关联。

代码首先检查缓存(缓存命中),看数据是否存在;如果不存在(缓存未命中),则查询数据库,然后缓存新数据以供后续调用(使用滑动时间跨度)。

通过构造函数注入 `IDistributedCache`,我无需为 `IDistributedCache` 方法实现任何功能。它们会自动实现;我需要做的就是告诉它在需要查询缓存时连接到哪个数据库(这在 API 项目中设置)。

例如,在 `SqlServerDistributedService` 类中,会将适当的属性传递给基类(在这种情况下是 `IDistributedCache`)。我还覆盖了基类属性(Logger 和 Database Context)——这种方法适用于每个服务。

EF Core DBContext

本教程我使用了两个不同的数据库作为数据源(SQL Server 和 MySQL),因此我创建了两个 DBContext 类。模型反映了数据库模式。

在 `DBContext` 类中需要注意的一点是,当您引用 appsetting 值时,实际上使用的是父级(在这种情况下是 API)的 appsetting。

MySQL 数据库

在此演示中,我将只使用 Employees 表。

SQL Server 数据库

代码配置

Redis

将以下包 Microsoft.Extensions.Caching.StackExchangeRedis 添加到您的项目中,以使用 Redis Cache 中间件。

在下面的代码片段中,您可以看到我正在注入我们的自定义服务类 `SqlServerDistributedService`,它继承自接口 `IDistributedService`(我们使用 SQL Server 作为数据源,Redis 作为分布式缓存系统)。

在自定义服务 `SqlServerDistributedService` 中,`IDistributedCache` 通过构造函数注入,因此我们可以通用地使用该缓存系统的 `DistributedCache` 中间件(基于 *program.cs* 中配置设置 `AddStackExchangeRedisCache`)。

NCache

将以下包 NCache.Microsoft.Extensions.Caching 添加到您的项目中,以使用 NCache 中间件。

在下面的代码片段中,您可以看到我正在注入我们的自定义服务类 `SqlServerDistributedService`,它继承自接口 `IDistributedService`(我们再次使用 SQL Server 作为数据源,NCache 作为分布式缓存系统)。

在自定义服务 `SqlServerDistributedService` 中,`IDistributedCache` 通过构造函数注入,因此我们可以通用地使用该缓存系统的 `DistributedCache` 中间件(基于 *program.cs* 中配置设置 `AddNCacheDistributedCache`)。

在 Web Portal 中监视活动

导航到 https://:8251/ClusteredCaches

然后点击“监视”按钮。

当您针对 NCache 运行演示时,您会看到缓存的峰值。

SqlCache

将以下包 Microsoft.Extensions.Caching.SqlServer 添加到您的项目中,以使用 SQLCache 中间件。

在下面的代码片段中,您可以看到我正在注入我们的自定义服务类 `SqlServerDistributedService`,它继承自接口 `IDistributedService`(我们再次使用 SQL Server 作为数据源,SQLCache 作为分布式缓存系统)。

在自定义服务 `SqlServerDistributedService` 中,`IDistributedCache` 通过构造函数注入,因此我们可以通用地使用该缓存系统的 `DistributedCache` 中间件(基于 program.cs 中配置设置 `AddDistributedSqlServerCache`)。

创建 SQL Server 缓存表

安装 NuGet 包后,下一步是为 .net CLI 中的 SQL-Cache 安装工具支持,以便我们可以生成所需的表,以便在 SQL Server 中保存缓存数据。

从 Visual Studio 打开程序包管理器控制台,然后输入以下命令。

dotnet tool install -g dotnet-sql-cache

 

现在您可以使用该工具创建数据库表,您将在其中存储缓存的条目(注意下面命令中 **dbo DistributedCacheTable** 之间的空格)。

dotnet sql-cache create "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=EmployeeDatabase;Trusted_Connection=True;MultipleActiveResultSets=true;" dbo DistributedCacheTable

SQL-Cache 的创建命令需要一个连接字符串、架构名称和一个将在目标数据库中创建的表名。

下面是 dotnet 工具创建的表的架构。

在下面的图片中,您可以看到 `DistributedCacheTable` 表中的缓存数据示例。

注意:绝对过期(2024-07-05 14:36:59.9846372 +00:00)- 如果我使用 IDistributedCache API 重新查询,缓存数据将不存在。但是如果我在 SQL Server 编辑器中重新查询 - 我仍然可以看到缓存数据!

在重新运行 API 请求后,数据被再次缓存,键相同但绝对过期日期不同。

MySqlCache

将以下包 Pomelo.Extensions.Caching.MySql 添加到您的项目中,以使用 Redis Cache 中间件。

在下面的代码片段中,您可以看到我正在注入我们的自定义服务类 `MySqlServerDistributedService`,它继承自接口 `IDistributedService`(这次我们使用 MySQL 作为数据源和分布式缓存系统)。

在自定义服务 `MySqlServerDistributedService` 中,`IDistributedCache` 通过构造函数注入,因此我们可以通用地使用该缓存系统的 `DistributedCache` 中间件(基于 *program.cs* 中配置设置 `AddDistributedMySqlCache`)。

创建 MySQL Server 缓存表

安装 NuGet 包后,下一步是为 .net CLI 中的 MySQL Cache 安装工具支持,以便我们可以生成所需的表,以便在 MySQL 中保存缓存数据。

从 Visual Studio 打开程序包管理器控制台,然后输入以下命令。

dotnet tool install --global Pomelo.Extensions.Caching.MySqlConfig.Tools 

现在您可以使用该工具创建数据库表,您将在其中存储缓存的条目。

dotnet mysql-cache create "server=localhost;user id=bertoneill;password=P@ssw0rd;port=3306;database=employeedatabase;Allow User Variables=True" "mysqlcache" --databaseName "employeedatabase"

`mysql-cache` 创建命令需要一个连接字符串、新的缓存表名和数据库名。

以下是 dotnet 工具创建的表。

在下面的图片中,您可以看到 mysqlcache 表中的缓存数据示例。

MongoDBCache

使用 SQL UI 工具,例如 MongoDB Compass,您可以创建数据库和集合(用于保存缓存数据)——下面,我创建了数据库 MongoCache 和集合 AppCache。

然后将以下包 MongoDbCache 添加到您的项目中,以使用 MongoDB Cache 中间件。

在下面的代码片段中,您可以看到我正在注入我们的自定义服务类 `MongoDBDistributedService`,它继承自接口 `IDistributedService`(我们再次使用 SQL Server 作为数据源,MongoDbCache 作为分布式缓存系统)。

在自定义服务 `MongoDBDistributedService` 中,`IDistributedCache` 通过构造函数注入,因此我们可以通用地使用该缓存系统的 `DistributedCache` 中间件(基于 *program.cs* 中配置设置 `AddMongoDbCache`)。

MongoDB 数据库中缓存的非 SQL 数据示例。

响应输出

您会将响应输出缓存与现有缓存系统结合使用,当 API 第一次被调用时,它会检查缓存(例如 Redis)以获取数据。当未找到(缓存未命中)时,将查询数据库并更新(Redis)缓存。然后将该数据返回给客户端,但也会被 API 本身缓存。

此时数据被缓存在两个地方,但您应该让您的缓存系统(Redis)比您的 API 持有数据更长时间。这样,由于缓存系统(Redis),您就不会那么频繁地命中数据库,而由于响应输出缓存,您的缓存系统也不应该那么频繁地被命中。

注意:您的 API(输出)缓存的持续时间应短于您缓存系统(例如 Redis)中的缓存时间。

为了更精细地控制每个 API(因为有些 API 可能需要更长的缓存时间),请创建适合每个 API 缓存持续时间的策略——如果没有使用策略,默认是 20 秒。

要为您的最小 API 启用响应缓存,您只需在 API 签名中添加 `[OutputCache]` 注释(并选择是否使用策略)。

您可以使用 SQL Profiler 来验证您的 API 响应输出缓存是否在重试时没有回写到数据库。另外,在缓存服务中设置一个断点,以验证直到响应输出持续时间过去之前,缓存是否没有被命中。

演示

启动 Redis 缓存系统

确保 Redis 缓存系统已启动(双击 redis-server)。

启动 Redis Insights

通过使用此 UI 工具,我们可以验证数据是否已添加到缓存中。

运行两个 Redis API

将两个 Redis API 设置为在运行解决方案时启动(右键单击解决方案并选择**属性**)。

这将启动两个 Swagger 浏览器——每个项目一个。

在 `SqlServerDistributedService` 类中,在 `RetrieveEmployeeByNameAsync` 函数中添加一个断点,就在 `IDistributedCache` 调用查询缓存机制以获取数据之后。

第一次进入时,缓存中找不到数据(缓存未命中),因此将查询数据库,并在 60 秒内缓存任何其他调用所需的数据。

运行第一个 API GetEmployeeByName,输入 Jane Doe 并点击 **Execute**。

代码路径会走到断点处,在这里您会看到 `cache` 属性是 NULL。

接下来,缓存将用数据库中的数据填充。

第一次 API 调用响应

运行第二个 API GetEmployeeByName,输入 Jane Doe 并点击 **Execute**。

现在当第二个 API 的代码路径命中断点时,`cache` 属性已填充。

SQL Server 缓存表

Redis Insights 缓存数据

如果看不到任何条目,请点击刷新链接。

性能提示

静态 JsonSerializerOptions 选项

当您扩展到生产环境,处理大量调用时,使用 `static JsonSerializerOptions` 和 `DistributedCacheEntryOptions` 会更有益,这些可以存储在内存中,无需为每次请求返回而创建。

private static JsonSerializerOptions _serializerOptions = new JsonSerializerOptions
{
    PropertyNamingPolicy = null,
    WriteIndented = true,
    AllowTrailingCommas = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

静态缓存选项

private static DistributedCacheEntryOptions _cacheOptions = new DistributedCacheEntryOptions
{
    AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(2), // Set absolute expiration to a specific date and time
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5), // Set relative expiration to 5 minutes from now
    SlidingExpiration = TimeSpan.FromMinutes(2) // Set sliding expiration to 2 minutes
};

缓存过期选项说明

AbsoluteExpiration

此属性指定缓存条目应过期并从缓存中移除的确切日期和时间。

AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(2)

在此示例中,缓存条目将在当前 UTC 时间后的 2 分钟精确过期,而无论上次访问时间如何。

当您有精确的过期要求时,使用 `AbsoluteExpiration`,例如希望缓存条目在特定日期的午夜过期。

AbsoluteExpirationRelativeToNow

此属性设置一个时间间隔,从缓存条目添加到缓存开始,在此之后缓存条目将过期。

AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)

在此,缓存条目将在添加到缓存 5 分钟后过期,无论后续访问如何。

对于一般的缓存场景,例如将数据从数据库获取后的 10 分钟内缓存,请使用 `AbsoluteExpirationRelativeToNow`。

SlidingExpiration

此属性定义一个时间间隔,每次访问缓存条目时都会重置。如果在该时间段内未访问该条目,则它将过期。

SlidingExpiration = TimeSpan.FromMinutes(2)

在此示例中,如果缓存条目连续 2 分钟未被访问,它将过期。每次访问都会重置过期计时器。

使用 `SlidingExpiration` 非常适合会话管理等场景,您希望只要数据被主动使用就将其保留在缓存中。它确保频繁访问的数据保留在缓存中,而未在指定时间内访问的数据会自动清除。这有助于有效地管理缓存内存,并通过使相关数据随时可用来提供更好的用户体验。

杂项

缩写

缩写 含义
EF 实体框架
EFC Entity Framework Core
SQL 结构化查询语言
TSQL 事务性结构化查询语言
LFU 最不常使用
LRU 最近最少使用
TTL 生存时间
© . All rights reserved.