ASP.NET Core 中的分布式缓存

作者:Mohsin Nasirsmandia

分布式缓存是由多个应用服务器共享的缓存,通常作为访问它的应用服务器的外部服务进行维护。 分布式缓存可以提高 ASP.NET Core 应用的性能和可伸缩性,尤其是当应用由云服务或服务器场托管时。

与其他将缓存数据存储在单个应用服务器上的缓存方案相比,分布式缓存具有多个优势。

当分发缓存数据时,数据:

  • 在多个服务器的请求之间保持一致(一致性)。
  • 在进行服务器重启和应用部署后仍然有效。
  • 不使用本地内存。

分布式缓存配置是特定于实现的。 本文介绍如何配置 SQL Server 和 Redis 分布式缓存。 还提供第三方实现,例如 NCacheGitHub 上的 NCache)。 无论选择哪种实现,应用都将使用 IDistributedCache 接口与缓存进行交互。

查看或下载示例代码如何下载

先决条件

若要使用 SQL Server 分布式缓存,请添加对 Microsoft.Extensions.Caching.SqlServer 包的包引用。

若要使用 Redis 分布式缓存,请添加对 Microsoft.Extensions.Caching.StackExchangeRedis 包的包引用。

若要使用 NCache 分布式缓存,请添加对 NCache.Microsoft.Extensions.Caching.OpenSource 包的包引用。

IDistributedCache 接口

IDistributedCache 接口提供以下方法来处理分布式缓存实现中的项:

  • GetGetAsync:如果在缓存中找到,则接受字符串键并以 byte[] 数组的形式检索缓存项。
  • SetSetAsync:使用字符串键将项(作为 byte[] 数组)添加到缓存。
  • RefreshRefreshAsync:根据键刷新缓存中的项,重置其可调到期超时(如果有)。
  • RemoveRemoveAsync:根据字符串键删除缓存项。

建立分布式缓存服务

Program.cs 中注册 IDistributedCache 实现。 本主题中所介绍的框架提供的实现包括:

分布式内存缓存

分布式内存缓存 (AddDistributedMemoryCache) 是框架提供的 IDistributedCache 实现,用于将项存储在内存中。 分布式内存缓存不是真正的分布式缓存。 缓存项由应用实例存储在运行该应用的服务器上。

分布式内存缓存是一个有用的实现:

  • 在开发和测试场景中。
  • 当在生产环境中使用单个服务器并且内存消耗不重要时。 实现分布式内存缓存会抽象缓存的数据存储。 如果需要多个节点或容错,它允许在未来实现真正的分布式缓存解决方案。

当应用在 Program.cs 的开发环境中运行时,示例应用使用分布式内存缓存:

builder.Services.AddDistributedMemoryCache();

分布式 SQL Server 缓存

分布式 SQL Server 缓存实现 (AddDistributedSqlServerCache) 允许分布式缓存使用 SQL Server 数据库作为其后备存储。 要在 SQL Server 实例中创建 SQL Server 缓存项表,可以使用 sql-cache 工具。 该工具使用指定的名称和架构来创建表。

通过运行 sql-cache create 命令在 SQL Server 中创建一个表。 提供 SQL Server 实例 (Data Source)、数据库 (Initial Catalog)、架构(例如 dbo)和表名(例如 TestCache):

dotnet sql-cache create "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=DistCache;Integrated Security=True;" dbo TestCache

将记录一条消息以指示该工具已成功:

Table and index were created successfully.

通过 sql-cache 工具创建的表具有以下架构:

SqlServer 缓存表

注意

应用应使用 IDistributedCache 实例(而不是 SqlServerCache)来处理缓存值。

示例应用在 Program.cs 的非开发环境中实现 SqlServerCache

builder.Services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString = builder.Configuration.GetConnectionString(
        "DistCache_ConnectionString");
    options.SchemaName = "dbo";
    options.TableName = "TestCache";
});

注意

ConnectionString ((可选)SchemaNameTableName) 通常存储在源代码管理外部 (,例如,机密管理器appsettings.json/appsettings.{Environment}.json或文件) 存储。 连接字符串可能包含应保留在源代码管理系统外部的凭据。

分布式 Redis 缓存

Redis 是一种开放源代码内存中数据存储,通常用作分布式缓存。 可以为 Azure 托管的 ASP.NET Core 应用配置 Azure Redis 缓存,并使用 Azure Redis 缓存进行本地开发。

应用使用 RedisCache 实例 (AddStackExchangeRedisCache) 来配置缓存实现。

  1. 创建 Azure Cache for Redis。
  2. 将主连接字符串 (StackExchange.Redis) 复制到配置
    • 本地开发:使用机密管理器保存连接字符串。
    • Azure:将连接字符串保存在应用服务配置或其他安全存储中。

以下代码启用 Azure Cache for Redis:

builder.Services.AddStackExchangeRedisCache(options =>
 {
     options.Configuration = builder.Configuration.GetConnectionString("MyRedisConStr");
     options.InstanceName = "SampleInstance";
 });

上述代码假定主连接字符串 (StackExchange.Redis) 以键名 MyRedisConStr 保存在配置中。

有关详细信息,请参阅 Azure Cache for Redis

有关本地 Redis 缓存的替代方法的讨论,请参阅此 GitHub 问题

分布式 NCache 缓存

NCache 是在 .NET 和 .NET Core 中以原生方式开发的开放源代码内存中分布式缓存。 NCache 既可以在本地工作,也可配置为在 Azure 或其他托管平台上运行的 ASP.NET Core 应用的分布式缓存群集。

要在本地计算机上安装和配置 NCache,请参阅Windows 入门指南(.NET 和 .NET Core)

若要配置 NCache,请执行以下操作:

  1. 安装 NCache 开放源代码 NuGet
  2. client.ncconf 中配置缓存群集。
  3. 将下列代码添加到 Program.cs
builder.Services.AddNCacheDistributedCache(configuration =>
{
    configuration.CacheName = "democache";
    configuration.EnableLogs = true;
    configuration.ExceptionsEnabled = true;
});

使用分布式缓存

要使用 IDistributedCache 接口,请在应用中请求 IDistributedCache 实例。 该实例由依赖项注入 (DI) 提供。

当示例应用启动时,会将 IDistributedCache 注入到 Program.cs。 当前时间是使用 IHostApplicationLifetime 缓存的(有关详细信息,请参阅通用主机:IHostApplicationLifetime):

app.Lifetime.ApplicationStarted.Register(() =>
{
    var currentTimeUTC = DateTime.UtcNow.ToString();
    byte[] encodedCurrentTimeUTC = System.Text.Encoding.UTF8.GetBytes(currentTimeUTC);
    var options = new DistributedCacheEntryOptions()
        .SetSlidingExpiration(TimeSpan.FromSeconds(20));
    app.Services.GetService<IDistributedCache>()
                              .Set("cachedTimeUTC", encodedCurrentTimeUTC, options);
});

示例应用将 IDistributedCache 注入 IndexModel,以供索引页面使用。

每次加载索引页面时,都会检查缓存中 OnGetAsync 中的缓存时间。 如果缓存时间尚未过期,则会显示该时间。 如果自上次访问缓存时间(上次加载此页面)以来已过去 20 秒,则页面显示“缓存时间已过期”。

选择“重置缓存时间”按钮,即可将缓存时间更新为当前时间。 该按钮将触发 OnPostResetCachedTime 处理程序方法。

public class IndexModel : PageModel
{
    private readonly IDistributedCache _cache;

    public IndexModel(IDistributedCache cache)
    {
        _cache = cache;
    }

    public string? CachedTimeUTC { get; set; }
    public string? ASP_Environment { get; set; }

    public async Task OnGetAsync()
    {
        CachedTimeUTC = "Cached Time Expired";
        var encodedCachedTimeUTC = await _cache.GetAsync("cachedTimeUTC");

        if (encodedCachedTimeUTC != null)
        {
            CachedTimeUTC = Encoding.UTF8.GetString(encodedCachedTimeUTC);
        }

        ASP_Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
        if (String.IsNullOrEmpty(ASP_Environment))
        {
            ASP_Environment = "Null, so Production";
        }
    }

    public async Task<IActionResult> OnPostResetCachedTime()
    {
        var currentTimeUTC = DateTime.UtcNow.ToString();
        byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);
        var options = new DistributedCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(20));
        await _cache.SetAsync("cachedTimeUTC", encodedCurrentTimeUTC, options);

        return RedirectToPage();
    }
}

对于具有内置实现的 IDistributedCache 实例,无需使用单一实例或范围内的生存期。

也可按需创建 IDistributedCache 实例,而不是使用 DI,但在代码中创建实例会使代码更难测试并且违反了显式依赖项原则

建议

在确定适合你的应用的最佳 IDistributedCache 实现时,请考虑以下几点:

  • 现有基础结构
  • 性能要求
  • Cost
  • 团队经验

缓存解决方案通常依靠内存中存储来提供对缓存数据的快速检索,但内存是一种有限的资源并且扩展成本很高。 仅将常用数据存储在缓存中。

通常,Redis 缓存比 SQL Server 缓存提供更高的吞吐量和更低的延迟。 但是,通常需要进行基准测试才能确定缓存策略的性能特征。

当将 SQL Server 用作分布式缓存后备存储时,如果将同一数据库用于缓存以及应用的普通数据存储和检索,这可能会对两者的性能产生负面影响。 建议对分布式缓存后备存储使用专用的 SQL Server 实例。

其他资源

分布式缓存是由多个应用服务器共享的缓存,通常作为访问它的应用服务器的外部服务进行维护。 分布式缓存可以提高 ASP.NET Core 应用的性能和可伸缩性,尤其是当应用由云服务或服务器场托管时。

与其他将缓存数据存储在单个应用服务器上的缓存方案相比,分布式缓存具有多个优势。

当分发缓存数据时,数据:

  • 在多个服务器的请求之间保持一致(一致性)。
  • 在进行服务器重启和应用部署后仍然有效。
  • 不使用本地内存。

分布式缓存配置是特定于实现的。 本文介绍如何配置 SQL Server 和 Redis 分布式缓存。 还提供第三方实现,例如 NCacheGitHub 上的 NCache)。 无论选择哪种实现,应用都将使用 IDistributedCache 接口与缓存进行交互。

查看或下载示例代码如何下载

先决条件

若要使用 SQL Server 分布式缓存,请添加对 Microsoft.Extensions.Caching.SqlServer 包的包引用。

若要使用 Redis 分布式缓存,请添加对 Microsoft.Extensions.Caching.StackExchangeRedis 包的包引用。

若要使用 NCache 分布式缓存,请添加对 NCache.Microsoft.Extensions.Caching.OpenSource 包的包引用。

IDistributedCache 接口

IDistributedCache 接口提供以下方法来处理分布式缓存实现中的项:

  • GetGetAsync:如果在缓存中找到,则接受字符串键并以 byte[] 数组的形式检索缓存项。
  • SetSetAsync:使用字符串键将项(作为 byte[] 数组)添加到缓存。
  • RefreshRefreshAsync:根据键刷新缓存中的项,重置其可调到期超时(如果有)。
  • RemoveRemoveAsync:根据字符串键删除缓存项。

建立分布式缓存服务

Startup.ConfigureServices 中注册 IDistributedCache 实现。 本主题中所介绍的框架提供的实现包括:

分布式内存缓存

分布式内存缓存 (AddDistributedMemoryCache) 是框架提供的 IDistributedCache 实现,用于将项存储在内存中。 分布式内存缓存不是真正的分布式缓存。 缓存项由应用实例存储在运行该应用的服务器上。

分布式内存缓存是一个有用的实现:

  • 在开发和测试场景中。
  • 当在生产环境中使用单个服务器并且内存消耗不重要时。 实现分布式内存缓存会抽象缓存的数据存储。 如果需要多个节点或容错,它允许在未来实现真正的分布式缓存解决方案。

当应用在 Startup.ConfigureServices 的开发环境中运行时,示例应用使用分布式内存缓存:

services.AddDistributedMemoryCache();

分布式 SQL Server 缓存

分布式 SQL Server 缓存实现 (AddDistributedSqlServerCache) 允许分布式缓存使用 SQL Server 数据库作为其后备存储。 要在 SQL Server 实例中创建 SQL Server 缓存项表,可以使用 sql-cache 工具。 该工具使用指定的名称和架构来创建表。

通过运行 sql-cache create 命令在 SQL Server 中创建一个表。 提供 SQL Server 实例 (Data Source)、数据库 (Initial Catalog)、架构(例如 dbo)和表名(例如 TestCache):

dotnet sql-cache create "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=DistCache;Integrated Security=True;" dbo TestCache

将记录一条消息以指示该工具已成功:

Table and index were created successfully.

通过 sql-cache 工具创建的表具有以下架构:

SqlServer 缓存表

注意

应用应使用 IDistributedCache 实例(而不是 SqlServerCache)来处理缓存值。

示例应用在 Startup.ConfigureServices 的非开发环境中实现 SqlServerCache

services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString = 
        _config["DistCache_ConnectionString"];
    options.SchemaName = "dbo";
    options.TableName = "TestCache";
});

注意

ConnectionString ((可选)SchemaNameTableName) 通常存储在源代码 (管理外部,例如,由机密管理器appsettings.json/appsettings.{Environment}.json或文件) 存储。 连接字符串可能包含应保留在源代码管理系统外部的凭据。

分布式 Redis 缓存

Redis 是一种开放源代码内存中数据存储,通常用作分布式缓存。 可以为 Azure 托管的 ASP.NET Core 应用配置 Azure Redis 缓存,并使用 Azure Redis 缓存进行本地开发。

应用使用 RedisCache 实例 (AddStackExchangeRedisCache) 来配置缓存实现。

  1. 创建 Azure Cache for Redis。
  2. 将主连接字符串 (StackExchange.Redis) 复制到配置
    • 本地开发:使用机密管理器保存连接字符串。
    • Azure:将连接字符串保存在应用服务配置或其他安全存储中。

以下代码启用 Azure Cache for Redis:

public void ConfigureServices(IServiceCollection services)
{
    if (_hostContext.IsDevelopment())
    {
        services.AddDistributedMemoryCache();
    }
    else
    {
        services.AddStackExchangeRedisCache(options =>
        {
            options.Configuration = _config["MyRedisConStr"];
            options.InstanceName = "SampleInstance";
        });
    }

    services.AddRazorPages();
}

上述代码假定主连接字符串 (StackExchange.Redis) 以键名 MyRedisConStr 保存在配置中。

有关详细信息,请参阅 Azure Cache for Redis

有关本地 Redis 缓存的替代方法的讨论,请参阅此 GitHub 问题

分布式 NCache 缓存

NCache 是在 .NET 和 .NET Core 中以原生方式开发的开放源代码内存中分布式缓存。 NCache 既可以在本地工作,也可配置为在 Azure 或其他托管平台上运行的 ASP.NET Core 应用的分布式缓存群集。

要在本地计算机上安装和配置 NCache,请参阅Windows 入门指南(.NET 和 .NET Core)

若要配置 NCache,请执行以下操作:

  1. 安装 NCache 开放源代码 NuGet

  2. client.ncconf 中配置缓存群集。

  3. 将下列代码添加到 Startup.ConfigureServices

    services.AddNCacheDistributedCache(configuration =>    
    {        
        configuration.CacheName = "demoClusteredCache";
        configuration.EnableLogs = true;
        configuration.ExceptionsEnabled = true;
    });
    

使用分布式缓存

要使用 IDistributedCache 接口,请从应用中的任何构造函数请求 IDistributedCache 实例。 该实例由依赖项注入 (DI) 提供。

当示例应用启动时,会将 IDistributedCache 注入到 Startup.Configure。 当前时间是使用 IHostApplicationLifetime 缓存的(有关详细信息,请参阅通用主机:IHostApplicationLifetime):

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, 
    IHostApplicationLifetime lifetime, IDistributedCache cache)
{
    lifetime.ApplicationStarted.Register(() =>
    {
        var currentTimeUTC = DateTime.UtcNow.ToString();
        byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);
        var options = new DistributedCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(20));
        cache.Set("cachedTimeUTC", encodedCurrentTimeUTC, options);
    });

示例应用将 IDistributedCache 注入 IndexModel,以供索引页面使用。

每次加载索引页面时,都会检查缓存中 OnGetAsync 中的缓存时间。 如果缓存时间尚未过期,则会显示该时间。 如果自上次访问缓存时间(上次加载此页面)以来已过去 20 秒,则页面显示“缓存时间已过期”。

选择“重置缓存时间”按钮,即可将缓存时间更新为当前时间。 该按钮将触发 OnPostResetCachedTime 处理程序方法。

public class IndexModel : PageModel
{
    private readonly IDistributedCache _cache;

    public IndexModel(IDistributedCache cache)
    {
        _cache = cache;
    }

    public string CachedTimeUTC { get; set; }

    public async Task OnGetAsync()
    {
        CachedTimeUTC = "Cached Time Expired";
        var encodedCachedTimeUTC = await _cache.GetAsync("cachedTimeUTC");

        if (encodedCachedTimeUTC != null)
        {
            CachedTimeUTC = Encoding.UTF8.GetString(encodedCachedTimeUTC);
        }
    }

    public async Task<IActionResult> OnPostResetCachedTime()
    {
        var currentTimeUTC = DateTime.UtcNow.ToString();
        byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);
        var options = new DistributedCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(20));
        await _cache.SetAsync("cachedTimeUTC", encodedCurrentTimeUTC, options);

        return RedirectToPage();
    }
}

注意

IDistributedCache 实例无需使用单一实例或范围内的生存期(至少对于内置实现而言)。

也可按需创建 IDistributedCache 实例,而不是使用 DI,但在代码中创建实例会使代码更难测试并且违反了显式依赖项原则

建议

在确定适合你的应用的最佳 IDistributedCache 实现时,请考虑以下几点:

  • 现有基础结构
  • 性能要求
  • Cost
  • 团队经验

缓存解决方案通常依靠内存中存储来提供对缓存数据的快速检索,但内存是一种有限的资源并且扩展成本很高。 仅将常用数据存储在缓存中。

通常,Redis 缓存比 SQL Server 缓存提供更高的吞吐量和更低的延迟。 但是,通常需要进行基准测试才能确定缓存策略的性能特征。

当将 SQL Server 用作分布式缓存后备存储时,如果将同一数据库用于缓存以及应用的普通数据存储和检索,这可能会对两者的性能产生负面影响。 建议对分布式缓存后备存储使用专用的 SQL Server 实例。

其他资源