本文介绍各种缓存机制。 缓存是将数据存储在中间层中的行为,使后续数据检索更快。 从概念上讲,缓存是性能优化策略和设计注意事项。 缓存可以通过使不经常更改(或检索成本高昂)数据更容易获得,从而显著提高应用性能。 本文介绍了三种缓存方法,并为每个方法提供示例源代码:
- Microsoft.Extensions.Caching.Memory:单服务器方案的内存中缓存
- Microsoft.Extensions.Caching.Hybrid:将内存中缓存和分布式缓存与附加功能相结合的混合缓存
- Microsoft.Extensions.Caching.Distributed:多服务器方案的分布式缓存
重要
.NET 中有两个 MemoryCache 类,一个在 System.Runtime.Caching 命名空间中,另一个在命名空间中 Microsoft.Extensions.Caching :
虽然本文重点介绍缓存,但它不包括 System.Runtime.Caching NuGet 包。 对 MemoryCache 的所有引用都在 Microsoft.Extensions.Caching 命名空间中。
所有Microsoft.Extensions.*包都已准备好依赖注入(DI)。
IMemoryCache
HybridCache和IDistributedCache接口可用作服务。
内存缓存
在本部分中,你将了解 Microsoft.Extensions.Caching.Memory 包。
IMemoryCache 的当前实现是 ConcurrentDictionary<TKey,TValue> 的包装器,公开功能丰富的 API。 缓存中的条目由ICacheEntry表示,且可以是任何object。 内存中缓存解决方案非常适合在单个服务器上运行的应用,其中缓存的数据在应用的进程中租用内存。
小窍门
对于多服务器缓存方案,请考虑 分布式缓存 方法作为内存中缓存的替代方法。
内存缓存 API
缓存的使用者可控制可调过期和绝对过期:
- ICacheEntry.AbsoluteExpiration
- ICacheEntry.AbsoluteExpirationRelativeToNow
- ICacheEntry.SlidingExpiration
设置过期会导致缓存中的条目在过期时间分配内未访问时 被逐出 。 消费者拥有额外的选项来通过 MemoryCacheEntryOptions 控制缓存条目。 每个ICacheEntry都与MemoryCacheEntryOptions配对,其中包含IChangeToken用于过期逐出功能,CacheItemPriority用于优先级设置,以及用于控制ICacheEntry.Size。 相关的扩展方法包括:
- MemoryCacheEntryExtensions.AddExpirationToken
- MemoryCacheEntryExtensions.RegisterPostEvictionCallback
- MemoryCacheEntryExtensions.SetSize
- MemoryCacheEntryExtensions.SetPriority
内存中缓存示例
若要使用默认 IMemoryCache 实现,请调用 AddMemoryCache 扩展方法,将所有必需的服务注册到 DI。 在以下代码示例中,泛型主机用于公开 DI 功能:
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
using IHost host = builder.Build();
根据 .NET 工作负载,可以采用不同的方式访问 IMemoryCache ,例如构造函数注入。 在此示例中,你在 IServiceProvider 上使用 host 实例,并调用泛型 GetRequiredService<T>(IServiceProvider) 扩展方法:
IMemoryCache cache =
host.Services.GetRequiredService<IMemoryCache>();
注册内存中缓存服务并通过 DI 解析后,即可开始缓存。 此示例遍历英文字母“A”到“Z”。 该 record AlphabetLetter 类型保存对字母的引用,并生成一条消息。
file record AlphabetLetter(char Letter)
{
internal string Message =>
$"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}
小窍门
在 file 类型上使用 AlphabetLetter 访问修饰符,因为它是在 Program.cs 文件中定义的,且只能从该文件进行访问。 有关详细信息,请参阅文件(C# 参考)。 若要查看完整的源代码,请参阅 Program.cs 部分。
此示例包含一个遍历字母表的辅助函数。
static async ValueTask IterateAlphabetAsync(
Func<char, Task> asyncFunc)
{
for (char letter = 'A'; letter <= 'Z'; ++letter)
{
await asyncFunc(letter);
}
Console.WriteLine();
}
在前述 C# 代码中:
-
Func<char, Task> asyncFunc在每次迭代时等待,并传递当前letter。 - 处理完所有字母后,会将一个空白行写入控制台。
若要将项添加到缓存,请调用其中 Create一个或 Set API:
var addLettersToCacheTask = IterateAlphabetAsync(letter =>
{
MemoryCacheEntryOptions options = new()
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
};
_ = options.RegisterPostEvictionCallback(OnPostEviction);
AlphabetLetter alphabetLetter =
cache.Set(
letter, new AlphabetLetter(letter), options);
Console.WriteLine($"{alphabetLetter.Letter} was cached.");
return Task.Delay(
TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd));
});
await addLettersToCacheTask;
在前述 C# 代码中:
- 变量
addLettersToCacheTask委托给IterateAlphabetAsync并等待。 -
Func<char, Task> asyncFunc使用 lambda 来表示。 -
MemoryCacheEntryOptions是以相对于现在的绝对过期来实例化的。 - 逐出后回叫已注册。
-
AlphabetLetter对象被实例化,并与Set和letter一起传入options。 - 该字母将作为缓存写入控制台。
- 最后,将返回 Task.Delay。
对于字母表中的每个字母,都会写入一个包含过期时间和逐出后回调函数的缓存条目。
逐出后回调将被逐出值的详细信息写入控制台:
static void OnPostEviction(
object key, object? letter, EvictionReason reason, object? state)
{
if (letter is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
}
};
缓存已被填充后,需要再次等待调用 IterateAlphabetAsync,但这次将调用 IMemoryCache.TryGetValue:
var readLettersFromCacheTask = IterateAlphabetAsync(letter =>
{
if (cache.TryGetValue(letter, out object? value) &&
value is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}");
}
return Task.CompletedTask;
});
await readLettersFromCacheTask;
如果cache包含letter键,且value是AlphabetLetter的实例,那么将写入控制台。 当letter键不在缓存中时,它将被逐出并调用其逐出回调。
其他扩展方法
IMemoryCache 具有许多方便的扩展方法,其中包括异步 GetOrCreateAsync:
- CacheExtensions.Get
- CacheExtensions.GetOrCreate
- CacheExtensions.GetOrCreateAsync
- CacheExtensions.Set
- CacheExtensions.TryGetValue
把所有的东西放在一起
整个示例应用源代码是一个顶级程序,需要两个 NuGet 包:
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
using IHost host = builder.Build();
IMemoryCache cache =
host.Services.GetRequiredService<IMemoryCache>();
const int MillisecondsDelayAfterAdd = 50;
const int MillisecondsAbsoluteExpiration = 750;
static void OnPostEviction(
object key, object? letter, EvictionReason reason, object? state)
{
if (letter is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
}
};
static async ValueTask IterateAlphabetAsync(
Func<char, Task> asyncFunc)
{
for (char letter = 'A'; letter <= 'Z'; ++letter)
{
await asyncFunc(letter);
}
Console.WriteLine();
}
var addLettersToCacheTask = IterateAlphabetAsync(letter =>
{
MemoryCacheEntryOptions options = new()
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
};
_ = options.RegisterPostEvictionCallback(OnPostEviction);
AlphabetLetter alphabetLetter =
cache.Set(
letter, new AlphabetLetter(letter), options);
Console.WriteLine($"{alphabetLetter.Letter} was cached.");
return Task.Delay(
TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd));
});
await addLettersToCacheTask;
var readLettersFromCacheTask = IterateAlphabetAsync(letter =>
{
if (cache.TryGetValue(letter, out object? value) &&
value is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}");
}
return Task.CompletedTask;
});
await readLettersFromCacheTask;
await host.RunAsync();
file record AlphabetLetter(char Letter)
{
internal string Message =>
$"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}
可以调整MillisecondsDelayAfterAdd和MillisecondsAbsoluteExpiration的值,观察缓存项在过期和逐出时行为的变化。 下面是运行此代码的示例输出。 (由于 .NET 事件的不确定性质,输出可能有所不同。
A was cached.
B was cached.
C was cached.
D was cached.
E was cached.
F was cached.
G was cached.
H was cached.
I was cached.
J was cached.
K was cached.
L was cached.
M was cached.
N was cached.
O was cached.
P was cached.
Q was cached.
R was cached.
S was cached.
T was cached.
U was cached.
V was cached.
W was cached.
X was cached.
Y was cached.
Z was cached.
A was evicted for Expired.
C was evicted for Expired.
B was evicted for Expired.
E was evicted for Expired.
D was evicted for Expired.
F was evicted for Expired.
H was evicted for Expired.
K was evicted for Expired.
L was evicted for Expired.
J was evicted for Expired.
G was evicted for Expired.
M was evicted for Expired.
N was evicted for Expired.
I was evicted for Expired.
P was evicted for Expired.
R was evicted for Expired.
O was evicted for Expired.
Q was evicted for Expired.
S is still in cache. The 'S' character is the 19 letter in the English alphabet.
T is still in cache. The 'T' character is the 20 letter in the English alphabet.
U is still in cache. The 'U' character is the 21 letter in the English alphabet.
V is still in cache. The 'V' character is the 22 letter in the English alphabet.
W is still in cache. The 'W' character is the 23 letter in the English alphabet.
X is still in cache. The 'X' character is the 24 letter in the English alphabet.
Y is still in cache. The 'Y' character is the 25 letter in the English alphabet.
Z is still in cache. The 'Z' character is the 26 letter in the English alphabet.
由于设置了绝对过期(MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow),因此最终将逐出所有缓存的项。
辅助角色服务缓存
缓存数据的一种常见策略是独立于使用的数据服务更新缓存。
工作服务模板是一个很好的示例,因为BackgroundService独立于其他应用程序代码运行(或在后台运行)。 当一个托管IHostedService实现的应用程序开始运行时,相应的实现(在这种情况下,BackgroundService或“worker”)将在同一进程中启动并运行。 这些托管服务通过 AddHostedService<THostedService>(IServiceCollection) 扩展方法向 DI 注册为单一实例。 可以使用任何服务生存期向 DI 注册其他服务。
重要
了解服务生存期非常重要。 调用 AddMemoryCache 注册内存中所有缓存服务时,这些服务将注册为单例。
照片服务场景
假设你要开发一个依赖于通过 HTTP 访问的第三方 API 的照片服务。 此照片的数据信息不会经常更改,但数量很大。 每张照片都用一个简单的 record 表示:
namespace CachingExamples.Memory;
public readonly record struct Photo(
int AlbumId,
int Id,
string Title,
string Url,
string ThumbnailUrl);
在以下示例中,会看到多个服务正在向 DI 注册。 每个服务都有一个责任。
using CachingExamples.Memory;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<CacheWorker>();
builder.Services.AddHostedService<CacheWorker>();
builder.Services.AddScoped<PhotoService>();
builder.Services.AddSingleton(typeof(CacheSignal<>));
using IHost host = builder.Build();
await host.StartAsync();
在前述 C# 代码中:
- 使用 默认值创建通用主机。
- 内存中缓存服务使用 AddMemoryCache 注册。
- 使用
HttpClient为CacheWorker类注册了一个 AddHttpClient<TClient>(IServiceCollection) 实例。 -
CacheWorker类使用 AddHostedService<THostedService>(IServiceCollection) 注册。 -
PhotoService类使用 AddScoped<TService>(IServiceCollection) 注册。 -
CacheSignal<T>类使用 AddSingleton 注册。 - 从
host生成器实例化并异步启动。
PhotoService 负责获取符合给定条件(或 filter)的照片:
using Microsoft.Extensions.Caching.Memory;
namespace CachingExamples.Memory;
public sealed class PhotoService(
IMemoryCache cache,
CacheSignal<Photo> cacheSignal,
ILogger<PhotoService> logger)
{
public async IAsyncEnumerable<Photo> GetPhotosAsync(Func<Photo, bool>? filter = default)
{
try
{
await cacheSignal.WaitAsync();
Photo[] photos =
(await cache.GetOrCreateAsync(
"Photos", _ =>
{
logger.LogWarning("This should never happen!");
return Task.FromResult(Array.Empty<Photo>());
}))!;
// If no filter is provided, use a pass-thru.
filter ??= _ => true;
foreach (Photo photo in photos)
{
if (!default(Photo).Equals(photo) && filter(photo))
{
yield return photo;
}
}
}
finally
{
cacheSignal.Release();
}
}
}
在前述 C# 代码中:
- 构造函数需要
IMemoryCache、CacheSignal<Photo>和ILogger。 -
GetPhotosAsync方法:- 定义参数
Func<Photo, bool> filter并返回一个IAsyncEnumerable<Photo>。 - 调用并等待
_cacheSignal.WaitAsync()释放;这可确保在访问缓存之前填充缓存。 - 调用
_cache.GetOrCreateAsync(),异步获取缓存中的所有照片。 - 该
factory参数记录警告并返回一个空的照片数组 - 这不应该发生。 - 缓存中的每个照片都使用
yield return迭代、筛选和具体化。 - 最后,将重置缓存信号。
- 定义参数
此服务的使用者可以自由调用 GetPhotosAsync 方法,并相应地处理照片。 不需要HttpClient,因为缓存已经包含照片。
异步信号基于一个封装的SemaphoreSlim实例,该实例位于一个受泛型类型约束的单例中。
CacheSignal<T>依赖于SemaphoreSlim的一个实例:
namespace CachingExamples.Memory;
public sealed class CacheSignal<T>
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
/// <summary>
/// Exposes a <see cref="Task"/> that represents the asynchronous wait operation.
/// When signaled (consumer calls <see cref="Release"/>), the
/// <see cref="Task.Status"/> is set as <see cref="TaskStatus.RanToCompletion"/>.
/// </summary>
public Task WaitAsync() => _semaphore.WaitAsync();
/// <summary>
/// Exposes the ability to signal the release of the <see cref="WaitAsync"/>'s operation.
/// Callers who were waiting, will be able to continue.
/// </summary>
public void Release() => _semaphore.Release();
}
在前面的 C# 代码中,修饰器模式用于包装该 SemaphoreSlim对象的实例。 由于 CacheSignal<T> 注册为单一实例,因此它可以在所有服务生存期中与任何泛型类型(在本例中为 Photo)一起使用。 它负责向缓存发出初始化信号。
CacheWorker 是 BackgroundService 的子类。
using System.Net.Http.Json;
using Microsoft.Extensions.Caching.Memory;
namespace CachingExamples.Memory;
public sealed class CacheWorker(
ILogger<CacheWorker> logger,
HttpClient httpClient,
CacheSignal<Photo> cacheSignal,
IMemoryCache cache) : BackgroundService
{
private readonly TimeSpan _updateInterval = TimeSpan.FromHours(3);
private bool _isCacheInitialized = false;
private const string Url = "https://jsonplaceholder.typicode.com/photos";
public override async Task StartAsync(CancellationToken cancellationToken)
{
await cacheSignal.WaitAsync();
await base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
logger.LogInformation("Updating cache.");
try
{
Photo[]? photos =
await httpClient.GetFromJsonAsync<Photo[]>(
Url, stoppingToken);
if (photos is { Length: > 0 })
{
cache.Set("Photos", photos);
logger.LogInformation(
"Cache updated with {Count:#,#} photos.", photos.Length);
}
else
{
logger.LogWarning(
"Unable to fetch photos to update cache.");
}
}
finally
{
if (!_isCacheInitialized)
{
cacheSignal.Release();
_isCacheInitialized = true;
}
}
try
{
logger.LogInformation(
"Will attempt to update the cache in {Hours} hours from now.",
_updateInterval.Hours);
await Task.Delay(_updateInterval, stoppingToken);
}
catch (OperationCanceledException)
{
logger.LogWarning("Cancellation acknowledged: shutting down.");
break;
}
}
}
}
在前述 C# 代码中:
- 构造函数需要
ILogger、HttpClient和IMemoryCache。 -
_updateInterval的定义适用于三个小时。 -
ExecuteAsync方法:- 在应用运行时循环。
- 发出
"https://jsonplaceholder.typicode.com/photos"HTTP 请求,并将响应映射为对象数组Photo。 - 照片组放置在
IMemoryCache键下的"Photos"中。 -
_cacheSignal.Release()被调用,释放任何正在等待信号的使用者。 - 根据更新间隔,等待对 Task.Delay 的调用。
- 延迟三个小时后,缓存将再次更新。
在同一过程中,消费者可以请求 IMemoryCache 提供照片,而 CacheWorker 负责更新缓存。
混合缓存
该 HybridCache 库结合了内存中缓存和分布式缓存的优势,同时解决现有缓存 API 的常见难题。 .NET 9 中引入的 HybridCache 提供了一个统一的 API,可简化缓存实现,并包括缓存风暴保护和可配置序列化等内置功能。
主要功能
HybridCache与单独使用IMemoryCache和IDistributedCache相比,提供了多种优势:
- 两级缓存:自动管理内存中(L1)和分布式(L2)缓存层。 首先从内存中缓存中检索数据,以加快速度,然后根据需要从分布式缓存中检索数据,最后从源中检索数据。
- Stampede 保护:防止多个并发请求执行相同的昂贵操作。 只有一个请求提取数据,而另一个请求则等待结果。
- 可配置序列化:支持多种序列化格式,包括 JSON(默认值)、protobuf 和 XML。
- 基于标记的失效:使用标记对相关的缓存条目进行分组,以便进行高效的批量失效。
-
简化的 API:该方法
GetOrCreateAsync会自动处理缓存未命中、序列化和存储。
何时使用 HybridCache
请考虑在以下情况下使用 HybridCache :
- 在多服务器环境中需要本地(内存中)和分布式缓存。
- 你需要针对缓存雪崩场景提供保护。
- 你更喜欢简化的 API 而不是手动协调
IMemoryCache和IDistributedCache。 - 对于相关条目,需要基于标签的缓存失效。
HybridCache 设置
若要使用 HybridCache,请安装 Microsoft.Extensions.Caching.Hybrid NuGet 包:
dotnet add package Microsoft.Extensions.Caching.Hybrid
调用 AddHybridCache 将 HybridCache 服务注册到 DI。
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHybridCache();
前面的代码使用默认选项进行注册 HybridCache 。 还可以配置全局选项:
var builderWithOptions = Host.CreateApplicationBuilder(args);
builderWithOptions.Services.AddHybridCache(options =>
{
options.MaximumPayloadBytes = 1024 * 1024; // 1 MB
options.MaximumKeyLength = 1024;
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5),
LocalCacheExpiration = TimeSpan.FromMinutes(2)
};
});
基本用法
主要方法是通过HybridCache与GetOrCreateAsync进行交互。 此方法检查缓存中是否有具有指定键的条目,如果未找到,则调用工厂方法来检索数据:
async Task<WeatherData> GetWeatherDataAsync(HybridCache cache, string city)
{
return await cache.GetOrCreateAsync(
$"weather:{city}",
async cancellationToken =>
{
// Simulate fetching from an external API
await Task.Delay(100, cancellationToken);
return new WeatherData(city, 72, "Sunny");
}
);
}
在前述 C# 代码中:
- 该方法
GetOrCreateAsync采用唯一键和工厂方法。 - 如果数据不在缓存中,则调用工厂方法以检索它。
- 数据自动存储在内存中缓存和分布式缓存中。
- 只有一个并发请求执行工厂方法;其他人等待结果。
条目选项
可以使用 HybridCacheEntryOptions 覆盖特定缓存条目的全局默认设置。
async Task<WeatherData> GetWeatherWithOptionsAsync(HybridCache cache, string city)
{
var entryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(10),
LocalCacheExpiration = TimeSpan.FromMinutes(5)
};
return await cache.GetOrCreateAsync(
$"weather:{city}",
async cancellationToken => new WeatherData(city, 72, "Sunny"),
entryOptions
);
}
条目设置选项允许你配置:
- HybridCacheEntryOptions.Expiration:应在分布式缓存中缓存条目的时长。
- HybridCacheEntryOptions.LocalCacheExpiration:条目应在本地内存中缓存多长时间。
- HybridCacheEntryOptions.Flags:用于控制缓存行为的附加标志。
基于标记的失效
标记允许对相关的缓存条目进行分组,并将它们失效。 这对于需要以单元的形式刷新相关数据的情况非常有用:
async Task<CustomerData> GetCustomerAsync(HybridCache cache, int customerId)
{
var tags = new[] { "customer", $"customer:{customerId}" };
return await cache.GetOrCreateAsync(
$"customer:{customerId}",
async cancellationToken => new CustomerData(customerId, "John Doe", "john@example.com"),
new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(30) },
tags
);
}
若要使具有特定标记的所有条目失效:
async Task InvalidateCustomerCacheAsync(HybridCache cache, int customerId)
{
await cache.RemoveByTagAsync($"customer:{customerId}");
}
还可以一次性使多个标记失效:
async Task InvalidateAllCustomersAsync(HybridCache cache)
{
await cache.RemoveByTagAsync(new[] { "customer", "orders" });
}
注释
基于标记的失效是一种逻辑操作。 它不会主动从缓存中删除值,但可确保标记的条目被视为缓存未命中。 条目最终会根据其配置的生存期过期。
删除缓存条目
若要按键删除特定缓存项,请使用 RemoveAsync 以下方法:
async Task RemoveWeatherDataAsync(HybridCache cache, string city)
{
await cache.RemoveAsync($"weather:{city}");
}
若要使所有缓存条目失效,请使用保留的通配符标记 "*":
async Task InvalidateAllCacheAsync(HybridCache cache)
{
await cache.RemoveByTagAsync("*");
}
Serialization
对于分布式缓存方案, HybridCache 需要序列化。 默认情况下,它会在内部处理 string 和 byte[],并使用 System.Text.Json 处理其他类型。 可以为特定类型配置自定义序列化程序,或使用常规用途序列化程序:
// Custom serialization example
// Note: This requires implementing a custom IHybridCacheSerializer<T>
var builderWithSerializer = Host.CreateApplicationBuilder(args);
builderWithSerializer.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(10),
LocalCacheExpiration = TimeSpan.FromMinutes(5)
};
});
// To add a custom serializer, uncomment and provide your implementation:
// .AddSerializer<WeatherData, CustomWeatherDataSerializer>();
配置分布式缓存
HybridCache 将配置的 IDistributedCache 实现用于其分布式 (L2) 缓存。 即使没有配置IDistributedCache,HybridCache仍提供内存缓存和踩踏保护。 将 Redis 添加为分布式缓存:
// Distributed cache with Redis
var builderWithRedis = Host.CreateApplicationBuilder(args);
builderWithRedis.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
});
builderWithRedis.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(30),
LocalCacheExpiration = TimeSpan.FromMinutes(5)
};
});
有关分布式缓存实现的详细信息,请参阅 分布式缓存。
分布式缓存
在某些情况下,需要分布式缓存,这种情况适用于多个应用服务器。 分布式缓存支持比内存中缓存方法更高的横向扩展。 使用分布式缓存将缓存内存卸载到外部进程,但确实需要额外的网络 I/O 并引入更多的延迟(即使名义上)。
分布式缓存抽象是 NuGet 包的 Microsoft.Extensions.Caching.Memory 一部分,甚至还有一个 AddDistributedMemoryCache 扩展方法。
谨慎
AddDistributedMemoryCache 应仅在开发或测试方案中使用, 而不是 可行的生产实现。
可以考虑以下包中的IDistributedCache的任何可用实现:
Microsoft.Extensions.Caching.SqlServerMicrosoft.Extensions.Caching.StackExchangeRedisNCache.Microsoft.Extensions.Caching.OpenSource
分布式缓存 API
分布式缓存 API 比内存缓存 API 更原始一些。 键值对更基本一些。 内存中缓存密钥基于一个 object,而分布式键是一个 string。 使用内存中缓存时,该值可以是任何强类型泛型值,而分布式缓存中的值将保留为 byte[]。 这并不是说各种实现不一定会公开强类型的泛型值,而是说这属于实现细节。
创建值
若要在分布式缓存中创建值,请调用其中一个集 API:
AlphabetLetter使用内存中缓存示例中的记录,可以将对象序列化为 JSON,然后将该string对象编码为byte[]:
DistributedCacheEntryOptions options = new()
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
};
AlphabetLetter alphabetLetter = new(letter);
string json = JsonSerializer.Serialize(alphabetLetter);
byte[] bytes = Encoding.UTF8.GetBytes(json);
await cache.SetAsync(letter.ToString(), bytes, options);
与内存缓存非常类似,缓存条目可以具有用于微调其在缓存中存在与否的选项,在本例中是DistributedCacheEntryOptions。
创建扩展方法
有多种基于方便的扩展方法可用于创建值。 这些方法有助于避免将对象的表示形式编码 string 为 byte[]:
读取值
若要从分布式缓存中读取值,请调用其中一个 Get API:
AlphabetLetter? alphabetLetter = null;
byte[]? bytes = await cache.GetAsync(letter.ToString());
if (bytes is { Length: > 0 })
{
string json = Encoding.UTF8.GetString(bytes);
alphabetLetter = JsonSerializer.Deserialize<AlphabetLetter>(json);
}
一旦从缓存中读取缓存项后,就可以从byte[]获取UTF8编码的string表示形式。
读取扩展方法
有多种基于方便的扩展方法可用于读取值。 这些方法有助于避免将对象解码 byte[] 为 string 表示形式:
更新值
无法通过单个 API 调用更新分布式缓存中的值。 相反,值可以使用其中一个刷新 API 重置其滑动过期时间:
如果需要更新实际值,则必须删除该值,然后重新添加该值。
删除值
若要删除分布式缓存中的值,请调用其中 Remove 一个 API:
小窍门
虽然这些 API 有同步版本,但请考虑分布式缓存的实现依赖于网络 I/O 的事实。 因此,通常最好使用异步 API。