ASP.NET Core 中的 HybridCache 库

重要

HybridCache目前仍处于预览状态,但在 .NET 扩展的未来次要版本中,将在 .NET 9.0 之后完全发布

本文介绍如何在 ASP.NET Core 应用中配置和使用 HybridCache 库。 有关库的简介,请参阅“缓存概述”的 HybridCache 部分

获取该库

安装 Microsoft.Extensions.Caching.Hybrid 包。

dotnet add package Microsoft.Extensions.Caching.Hybrid --version "9.0.0-preview.7.24406.2"

注册服务

通过调用 AddHybridCache,将 HybridCache 服务添加到依赖项注入 (DI) 容器:

// Add services to the container.
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddAuthorization();

builder.Services.AddHybridCache();

前面的代码使用默认选项注册 HybridCache 服务。 注册 API 还可以配置选项序列化

获取和存储缓存条目

HybridCache 服务提供一个带有两个重载的 GetOrCreateAsync 方法,接受一个密钥和:

  • 工厂方法。
  • 状态和工厂方法。

该方法使用密钥尝试从主缓存中检索对象。 如果在主缓存中找不到该项(缓存未命中),则它会检查辅助缓存(如果已配置)。 如果在此处找不到数据(另一个缓存未命中),它会调用工厂方法来从数据源获取对象。 然后,它将对象存储在主缓存和辅助缓存中。 如果在主缓存或辅助缓存中找到对象(缓存命中),则永远不会调用工厂方法。

HybridCache 服务确保只有一个给定键的并发调用者调用工厂方法,所有其他调用者都等待该调用的结果。 传递给 GetOrCreateAsyncCancellationToken 表示组合取消所有并发调用者。

主要 GetOrCreateAsync 重载

对于大多数场景,推荐 GetOrCreateAsync 的无状态重载。 要调用它的代码相对简单。 下面是一个示例:

public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
    {
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
            cancellationToken: token
        );
    }

    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
    {
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}

替代的 GetOrCreateAsync 重载

替代重载可能会降低捕获的变量和每个实例回调的一些开销,但代价是代码更复杂。 在大多数情况下,性能提升不会抵消代码的复杂性。 下面是使用替代重载的示例:

public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
    {
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            (name, id, obj: this),
            static async (state, token) =>
            await state.obj.GetDataFromTheSourceAsync(state.name, state.id, token),
            cancellationToken: token
        );
    }

    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
    {
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}

SetAsync 方法

在许多情况下,GetOrCreateAsync 是唯一需要的 API。 但是,HybridCache 还有 SetAsync 来将对象存储在缓存中,而无需先尝试检索它。

按键移除缓存条目

当某个缓存条目的底层数据在过期前发生更改时,请通过使用条目的键调用 RemoveAsync 来显式移除该条目。 使用重载可以指定键值的集合。

移除某个条目时,会从主缓存和辅助缓存中移除该条目。

按标记移除缓存条目

标记可用于将缓存条目分组并使它们失效。

在调用 GetOrCreateAsync 时设置标记,如以下示例所示:

public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
    {
        var tags = new List<string> { "tag1", "tag2", "tag3" };
        var entryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromMinutes(1),
            LocalCacheExpiration = TimeSpan.FromMinutes(1)
        };
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
            entryOptions,
            tags,
            cancellationToken: token
        );
    }
    
    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
    {
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}

通过使用标记值调用 RemoveByTagAsync 移除指定标记的所有条目。 使用重载可以指定标记值的集合。

移除某个条目时,会从主缓存和辅助缓存中移除该条目。

选项

可使用 AddHybridCache 方法来配置全局默认值。 以下示例显示如何配置一些可用选项:

// Add services to the container.
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();

builder.Services.AddHybridCache(options =>
    {
        options.MaximumPayloadBytes = 1024 * 1024;
        options.MaximumKeyLength = 1024;
        options.DefaultEntryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromMinutes(5),
            LocalCacheExpiration = TimeSpan.FromMinutes(5)
        };
    });

GetOrCreateAsync 方法还可以采用 HybridCacheEntryOptions 对象来替代特定缓存条目的全局默认值。 下面是一个示例:

public class SomeService(HybridCache cache)
{
    private HybridCache _cache = cache;

    public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
    {
        var tags = new List<string> { "tag1", "tag2", "tag3" };
        var entryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromMinutes(1),
            LocalCacheExpiration = TimeSpan.FromMinutes(1)
        };
        return await _cache.GetOrCreateAsync(
            $"{name}-{id}", // Unique key to the cache entry
            async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
            entryOptions,
            tags,
            cancellationToken: token
        );
    }
    
    public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
    {
        string someInfo = $"someinfo-{name}-{id}";
        return someInfo;
    }
}

有关这些选项的详细信息,请查看源代码:

限制

通过 HybridCacheOptions 的下列属性,可配置适用于所有缓存条目的限制:

  • MaximumPayloadBytes - 缓存条目的最大大小。 默认值为 1 MB。 存储超过此大小的值的尝试将被记录,并且该值不会存储在缓存中。
  • MaximumKeyLength - 缓存键的最大长度。 默认值为 1024 个字符。 存储超过此大小的值的尝试将被记录,并且该值不会存储在缓存中。

序列化

使用辅助、进程外缓存需要序列化。 序列化配置为注册 HybridCache 服务的一部分。 可以通过从 AddHybridCache 调用链接的 AddSerializerAddSerializerFactory 方法配置特定于类型和常规用途的序列化程序。 默认情况下,库在内部处理 stringbyte[],并使用 System.Text.Json 处理其他所有内容。 HybridCache 还可以使用其他序列化程序,例如 protobuf 或 XML。

以下示例将服务配置为使用特定于类型的 protobuf 序列化程序:

// Add services to the container.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();

builder.Services.AddHybridCache(options =>
    {
        options.DefaultEntryOptions = new HybridCacheEntryOptions
        {
            Expiration = TimeSpan.FromSeconds(10),
            LocalCacheExpiration = TimeSpan.FromSeconds(5)
        };
    }).AddSerializer<SomeProtobufMessage, 
        GoogleProtobufSerializer<SomeProtobufMessage>>();

以下示例将服务配置为使用可处理许多 protobuf 类型的常规用途 protobuf 序列化程序:

// Add services to the container.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();

builder.Services.AddHybridCache(options =>
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromSeconds(10),
        LocalCacheExpiration = TimeSpan.FromSeconds(5)
    };
}).AddSerializerFactory<GoogleProtobufSerializerFactory>();

辅助缓存需要数据存储,例如 Redis 或 SqlServer。 若要使用 Azure Redis 缓存,例如:

  • 安装 Microsoft.Extensions.Caching.StackExchangeRedis 包。

  • 创建 Azure Cache for Redis 的实例。

  • 获取连接到 Redis 实例的连接字符串。 通过在 Azure 门户中概述页上选择“显示访问密钥”来查找连接字符串。

  • 将连接字符串存储在应用的配置中。 例如,使用类似于以下 JSON 的用户机密文件,并在 ConnectionStrings 节中使用连接字符串。 将 <the connection string> 替换为实际的连接字符串:

    {
      "ConnectionStrings": {
        "RedisConnectionString": "<the connection string>"
      }
    }
    
  • 在 DI 中注册 Redis 包提供的 IDistributedCache 实现。 为此,请调用 AddStackExchangeRedisCache 并传入连接字符串。 例如:

    builder.Services.AddStackExchangeRedisCache(options =>
    {
        options.Configuration = 
            builder.Configuration.GetConnectionString("RedisConnectionString");
    });
    
  • Redis IDistributedCache 实现现已可从应用的 DI 容器获取。 HybridCache 将其用作辅助缓存,并使用为其配置的序列化程序。

有关详细信息,请参阅 HybridCache 序列化示例应用

缓存存储

默认情况下,HybridCache 为其主缓存存储使用 MemoryCache。 缓存条目存储在进程内,因此每个服务器都有一个单独缓存,该缓存在每次重启服务器进程时都会丢失。 对于辅助进程外存储(例如 Redis 或 SQL Server),HybridCache 使用所配置的 IDistributedCache 实现(如果有)。 但是,即使没有 IDistributedCache 实现,HybridCache 服务仍提供进程内缓存和踩踏保护。

优化性能

若要优化性能,请将 HybridCache 配置为重复使用对象,并避免 byte[] 分配。

重用对象

通过重用实例,HybridCache 可以减少与每次调用反序列化相关的 CPU 和对象分配开销。 在缓存对象较大或被经常访问的情况下,这会提高性能。

在使用 IDistributedCache 的典型现有代码中,每次从缓存中检索对象都会导致反序列化。 此行为意味着每个并发调用方都获取一个单独的对象实例,该实例无法与其他实例交互。 这实现了线程安全性,因为并发修改同一对象实例没有风险。

将根据现有 IDistributedCache 代码调整对 HybridCache 的大量使用,因此 HybridCache 在默认情况下会保留此行为,以避免引入并发 bug。 但是,如果出现以下情况,对象本质上是线程安全的:

  • 这些类型是不可变的。
  • 代码不会修改它们。

在这种情况下,请通知 HybridCache,使其知道通过以下方法重用实例很安全:

  • 将类型标记为 sealed。 C# 中的 sealed 关键字表示类无法被继承。
  • [ImmutableObject(true)] 属性应用于类型。 [ImmutableObject(true)] 属性指示创建对象后无法更改该对象的状态。

避免 byte[] 分配

HybridCache 还提供用于 IDistributedCache 实现的可选 API,以避免 byte[] 分配。 此功能由 Microsoft.Extensions.Caching.StackExchangeRedisMicrosoft.Extensions.Caching.SqlServer 包的预览版本实现。 有关详细信息,请参阅 IBufferDistributedCache。下面是用于安装包的 .NET CLI 命令:

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis --prerelease
dotnet add package Microsoft.Extensions.Caching.SqlServer --prerelease

自定义 HybridCache 实现

HybridCache 抽象类的具体实现包含在共享框架中,并通过依赖项注入提供。 不过,欢迎开发人员提供 API 的自定义实现。

兼容性

HybridCache 库支持较旧的 .NET 运行时,最低支持 .NET Framework 4.7.2 和 .NET Standard 2.0。

其他资源

有关 HybridCache 的详细信息,请参阅下列资源: