ASP.NET Core 使用 StackExchangeRedis 包构建 Redis 分布式缓存:IDistributedCache 配置与实现深度解析
2026/9/9 20:51:05 网站建设 项目流程

ASP.NET Core 使用 StackExchangeRedis 包构建 Redis 分布式缓存:IDistributedCache 配置与实现深度解析

【免费下载链接】aspnetcoreASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.项目地址: https://gitcode.com/GitHub_Trending/as/aspnetcore

Microsoft.Extensions.Caching.StackExchangeRedis是 ASP.NET Core(dotnet/aspnetcore)仓库中提供的 Redis 分布式缓存实现,它基于 StackExchange.Redis 客户端完整实现了IDistributedCache接口。本指南围绕仓库中的 PACKAGE.md 展开,从安装注册、选项配置、底层存储模型到连接与故障恢复机制,带你掌握在 ASP.NET Core 应用中接入 Redis 分布式缓存、进行多实例键隔离与精细过期策略的完整实战方案。

一、该包是什么:定位与源码结构

Microsoft.Extensions.Caching.StackExchangeRedis提供的是Microsoft.Extensions.Caching.Distributed.IDistributedCache的一个基于 Redis 的分布式缓存实现。所谓“分布式缓存”,是指缓存数据存放在独立的进程或服务(此处即 Redis)中,供多个应用实例共享——这与内存缓存(IMemoryCache)不同,它天然支持横向扩展与多副本一致读取。

在仓库中,该包的完整实现位于 src/Caching/StackExchangeRedis 目录,其src子目录下源码结构如下:

src/Caching/StackExchangeRedis/ ├── src/ │ ├── Microsoft.Extensions.Caching.StackExchangeRedis.csproj # 包工程文件 │ ├── PACKAGE.md # 包说明文档 │ ├── RedisCache.cs # 核心实现(RedisCache 类型) │ ├── RedisCache.Log.cs # 基于源生成器的日志定义 │ ├── RedisCacheImpl.cs # 内部 DI 实现(支持 HybridCache 感知) │ ├── RedisCacheOptions.cs # 配置选项 │ └── StackExchangeRedisCacheServiceCollectionExtensions.cs # AddStackExchangeRedisCache 扩展方法 └── test/ # 单元测试与 Redis 测试基础设施

包工程文件 Microsoft.Extensions.Caching.StackExchangeRedis.csproj 中声明其描述为 "Distributed cache implementation of Microsoft.Extensions.Caching.Distributed.IDistributedCache using Redis.",并以IsPackable=trueIsShipping=true作为正式发布包产出。

二、安装

与所有 NuGet 包一致,安装命令为:

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

从 工程文件 的TargetFrameworks可以看出,该包同时面向net10.0DefaultNetCoreTargetFramework)、当前 LTS 目标框架、.NET Framework(DefaultNetFxTargetFramework)以及netstandard2.0,因此可以用于传统 .NET Framework 应用、现代 .NET Core/.NET 应用以及类库项目中。它的核心外部依赖仅有一个:StackExchange.Redis客户端(见该文件的<Reference Include="StackExchange.Redis" />)。

三、快速开始:注册与使用

3.1 通过 AddStackExchangeRedisCache 完成 DI 注册

Program.cs中使用AddStackExchangeRedisCache扩展方法注册服务即可。这是 PACKAGE.md 提供的标准示例:

var builder = WebApplication.CreateBuilder(); builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = builder.Configuration.GetConnectionString("MyRedisConStr"); options.InstanceName = "MyCache"; }); var app = builder.Build();

要点说明:

  • Configuration接收的是 Redis 连接配置字符串(例如"localhost:6379"),可直接绑定appsettings.jsonConnectionStrings:MyRedisConStr配置项的值;
  • InstanceName用于为键添加前缀,其核心价值是让多个应用/服务共享同一个 Redis 后端时互不干扰(详见第六节);
  • 该方法以扩展方法的形式位于命名空间Microsoft.Extensions.DependencyInjection下(实现见 StackExchangeRedisCacheServiceCollectionExtensions.cs),注册时依次完成三件事:
services.AddOptions(); services.Configure(setupAction); services.Add(ServiceDescriptor.Singleton<IDistributedCache, RedisCacheImpl>());

也就是说,缓存实例以单例(Singleton)生命周期注册到容器,容器解析IDistributedCache时拿到的是RedisCacheImplRedisCache的内部子类)。测试 CacheServiceExtensionsTests.cs 中AddStackExchangeRedisCache_RegistersDistributedCacheAsSingleton用例验证了这一行为,而AddStackExchangeRedisCache_allows_chaining则验证了该方法支持链式调用(返回同一个IServiceCollection)。

3.2 在业务代码中注入与读写

注册完成后,通过构造器注入IDistributedCache即可使用。由于IDistributedCache面向的是byte[],字符串读写需要借助Encoding.UTF8完成编解码:

public class WeatherService(IDistributedCache cache) { public async Task<string?> GetWeatherAsync(string city, CancellationToken token = default) { var key = $"weather:{city}"; var bytes = await cache.GetAsync(key, token); return bytes is null ? null : Encoding.UTF8.GetString(bytes); } public async Task SetWeatherAsync(string city, string payload, CancellationToken token = default) { var key = $"weather:{city}"; await cache.SetAsync(key, Encoding.UTF8.GetBytes(payload), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }, token); } }

3.3 appsettings.json 中管理连接配置

实践中推荐把连接串放在配置文件中,保持注册代码的简洁:

{ "ConnectionStrings": { "MyRedisConStr": "localhost:6379,password=yourpassword,defaultDatabase=0" } }

注意:ConnectionString 本身由 RedisCacheOptions.cs 中GetConfiguredOptions()方法交给 StackExchange.Redis 的ConfigurationOptions.Parse解析,因此支持 StackExchange.Redis 的所有标准配置项(主机、端口、密码、defaultDatabasessl、超时等)。

四、核心类型

PACKAGE.md 中列举了两个主要类型,它们是理解该包的门户:

类型职责
RedisCache基于 Redis 的分布式缓存核心实现。其类注释明确说明内部使用 StackExchange.Redis 作为 Redis 客户端(见 RedisCache.cs)。它实现了IBufferDistributedCacheIDistributedCache的增强版本,支持将数据直接写入IBufferWriter<byte>)并实现IDisposable
RedisCacheOptions用于配置RedisCache的选项类,同时实现了IOptions<RedisCacheOptions>(见 RedisCacheOptions.cs)

从已发布公共 API 清单(PublicAPI.Shipped.txt)可以看到,RedisCache对外暴露的成员即标准分布式缓存操作:Get/GetAsyncSet/SetAsyncRefresh/RefreshAsyncRemove/RemoveAsync,以及构造函数RedisCache(IOptions<RedisCacheOptions>)Dispose()

五、RedisCacheOptions:六大可配置项详解

RedisCacheOptions.cs 是配置的入口,除ConfigurationInstanceName外还提供了多个高级选项,官方公共 API 中公开的可写属性总结如下:

属性类型说明
Configurationstring?连接 Redis 的配置字符串,示例"localhost:6379"。最终会被ConfigurationOptions.Parse解析
ConfigurationOptionsStackExchange.Redis.ConfigurationOptions?以强类型对象方式提供的连接配置,文档注释注明“优先于Configuration”。二者同时设置时,ConfigurationOptions生效
InstanceNamestring?Redis 实例名,用于让单个后端缓存被多个应用/服务分区共享;设置后缓存键会以该值为前缀
ConnectionMultiplexerFactoryFunc<Task<IConnectionMultiplexer>>?创建ConnectionMultiplexer实例的委托工厂。适合需要完全自定义连接复用器创建过程(如预配置ConfigurationOptions、连接前校验、接入自定义多路复用器)的场景
ProfilingSessionFunc<ProfilingSession>?注册 StackExchange.Redis 性能分析会话工厂。设置后连接建立时会通过connection.RegisterProfiler(...)挂接(见 RedisCache.cs),可用于采集 Redis 命令级耗时指标
UseForceReconnect(内部属性)bool无法直接通过对象初始化器设置,但可通过AppContext 开关Microsoft.AspNetCore.Caching.StackExchangeRedis.UseForceReconnect启用“强制重连”容错模式(见下文第八节)

5.1 一个必知细节:AbortOnConnectFail 被强制关闭

在 RedisCacheOptions.cs 的GetConfiguredOptions()中可以看到:

var options = ConfigurationOptions ?? ConfigurationOptions.Parse(Configuration!); // we don't want an initially unavailable server to prevent DI creating the service itself options.AbortOnConnectFail = false; return options;

无论你在连接字符串里是否设置了abortConnect=false,实现都会强制将AbortOnConnectFail设为false。这样做的目的正如注释所说:Redis 服务暂时不可用时,DI 容器仍然可以正常创建缓存服务,不会因连接失败阻断应用启动。连接会转入后台重试,命令在恢复后自动可用。

5.2 使用 ConfigurationOptions 的注册示例

当需要以强类型方式(而非字符串)配置连接时,写法如下:

builder.Services.AddStackExchangeRedisCache(options => { options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions { EndPoints = { "myredis.contoso.com", 6380 }, Password = "******", Ssl = true, AbortOnConnectFail = false, ConnectTimeout = 5000, SyncTimeout = 5000, }; options.InstanceName = "MyCache"; });

六、InstanceName:多应用共享 Redis 的键前缀隔离

当多个应用(或多环境)共用同一个 Redis 实例时,InstanceName提供了一种简单可靠的隔离手段。

在 RedisCache.cs 的构造函数中可以看到具体实现:

var instanceName = _options.InstanceName; if (!string.IsNullOrEmpty(instanceName)) { _instancePrefix = (RedisKey)Encoding.UTF8.GetBytes(instanceName); }

InstanceName会被 UTF-8 编码成字节形式的前缀。所有键操作(读写、刷新、删除)都会经过_instancePrefix.Append(key)处理,例如构造缓存键为"MyCacheweather:Shanghai"。从源码注释看,这里特意提前把前缀编码为byte[],是为了帮助 StackExchange.Redis 在键前缀拼接场景下避免重复分配与额外的 UTF-8 编码开销——代码以字节形态存放前缀即可让后续拼接“零转换”。

需要注意,前缀不是Redis 的命名空间或者 DB 号,它只是拼接在键前面的普通字符串。因此如果两个应用设置了相同的InstanceName,它们仍会互相看到对方的数据,请确保不同应用使用互不相同的实例名。

七、底层存储模型:一个缓存项 = 一个 Redis Hash

理解RedisCache的实现,关键在于知道它在 Redis 中如何组织数据。与简单SET/GET一个字符串不同,RedisCache将每个缓存项存储为一个Redis Hash,内部使用三个固定字段(见 RedisCache.cs):

哈希字段名含义
absexp绝对过期时间(AbsoluteExpiration),以DateTimeOffset.Ticks存储;无绝对过期时写入哨兵值-1(源码中常量NotPresent
sldexp滑动过期时长(SlidingExpiration),以TimeSpan.Ticks存储;无滑动过期时同样写入-1
data用户实际写入的缓存负载(byte[]

一次Set操作在 SetImpl 中完成:先计算 TTL,再构造包含上述三个字段的HashEntry[]。当不需要 TTL 时直接执行HashSet当需要 TTL 时,实现借助 StackExchange.Redis 的批处理(Batch)管道把两个命令一起发出:

var batch = cache.CreateBatch(); var setFields = batch.HashSetAsync(prefixedKey, fields); var setTtl = batch.KeyExpireAsync(prefixedKey, TimeSpan.FromSeconds(ttl.GetValueOrDefault())); batch.Execute(); cache.WaitAll(setFields, setTtl);

异步路径(SetImplAsync)则通过Task.WhenAll(HashSetAsync, KeyExpireAsync)并行发出。这里使用**秒级 TTL(KeyExpire而非KeyExpire的毫秒版)**是刻意的设计取舍,源码注释解释:TTL 以整数秒计算可兼容更多 Redis 服务端实现(见 GetExpirationInSeconds)。

7.1 读取即刷新的滑动过期语义

每次Get/Refresh内部都走GetAndRefreshGetAndRefreshAsync(见 RedisCache.cs)。它一次性读取所需的哈希字段(absexpsldexp,需要时再加data),然后通过MapMetadata还原两个过期时间:

  • 若只存在绝对过期,则不做额外操作;
  • 若存在滑动过期,则重新执行KeyExpire把键的 TTL 重置为滑动时长(相当于 LRU 命中刷新)。

有趣的是源码里保留了GetAndRefresh的 TODO 注释:能否把这套“读取 + 元数据解析 + 可能刷新过期”的逻辑在 Redis 服务端一步完成。目前的实现方式是:滑动过期会把过期时间刷新为min(距绝对过期剩余时间, 滑动时长)(见 RedisCache.Refresh),从而保证滑动刷新永远不会越过绝对过期边界。

7.2 过期的校验与计算

写入时的过期时间由 GetAbsoluteExpiration 与 GetExpirationInSeconds 完成。前者有一个容易踩坑的约束:如果AbsoluteExpiration已经是过去时间,会直接抛出ArgumentOutOfRangeException,消息为 "The absolute expiration value must be in the future."。测试文件 TimeExpirationTests.cs 中的AbsoluteExpirationInThePastThrowsAbsoluteExpirationExpiresAbsoluteSubSecondExpirationExpiresImmediately等用例正是围绕这套过期语义进行验证的(这些用例在无 Redis 环境时默认被跳过,需要本地启动 Redis 并将RedisTestConfig.RedisPort调为实际端口)。

过期时长优先级上,AbsoluteExpirationRelativeToNow会直接换算为绝对过期时间点,随后在绝对与滑动同时存在时取两者的较近者作为 TTL(Math.Min)。

八、连接管理、懒连接与强制重连容错

RedisCache采用懒连接策略:真正的ConnectionMultiplexer直到第一次读写操作时才建立。连接对象缓存于字段_cache,并用SemaphoreSlim(_connectionLock)保证并发安全(见 Connect 与 ConnectSlowAsync)。同时,RedisCache还实现了IDisposable,在Dispose()中会把当前连接关闭并释放。

为提升可观测性与诊断效率,PrepareConnection 在每次连接建立后还会:

  1. 注册性能分析会话(若配置了ProfilingSession);
  2. 通过AddLibraryNameSuffix("aspnet")AddLibraryNameSuffix("DC")为连接附加库名后缀,帮助识别 ASP.NET Core 缓存产生的流量(DC表示 Distributed Cache)。若检测到应用同时注册了HybridCache,还会追加"HC"后缀。这一调用可能抛出的异常被记录为 Debug 级日志UnableToAddLibraryNameSuffix(定义见 RedisCache.Log.cs)。

8.1 强制重连模式(UseForceReconnect)

StackExchange.Redis 自身具备自动重连能力,但在个别网络环境下(如 Azure Redis 的故障转移场景)仍可能出现客户端复用陈旧连接的问题。为此实现提供了一条注释中引用的 “force reconnect” 最佳实践路径(见 RedisCache.cs),可以通过 AppContext 开关启用:

AppContext.SetSwitch("Microsoft.AspNetCore.Caching.StackExchangeRedis.UseForceReconnect", isEnabled: true);

启用后,每次 Redis 操作抛出的异常会经过 OnRedisError 处理,只有异常属于RedisConnectionExceptionSocketException时才可能触发顶层重连,且遵循两个时间阈值:

  • ReconnectMinInterval = 60s:距上次连接/重连不足 60 秒时主动重建连接,避免与 StackExchange.Redis 内部重连机制“打架”;
  • ReconnectErrorThreshold = 30s:只有当错误已经持续至少 30 秒、且最近一次错误也落在 30 秒窗口内时,才会真正重建(防止基于“陈旧错误”误触发)。若错误中断超过 30 秒,计时会复位重新累计。

判定通过后,实现会以Interlocked.CompareExchange将共享的_cache字段清空并释放旧连接,下一次调用自然会走懒连接逻辑重新建立全新连接。这层重连只在RedisCache层做“顶层兜底”,StackExchange.Redis 内部的自动重连仍是第一道防线。

九、与 HybridCache 的协同

RedisCacheImpl(RedisCacheImpl.cs)作为 DI 注册的实现类,额外注入了IServiceProviderIsService用于检测容器中是否注册了Microsoft.Extensions.Caching.Hybrid.HybridCache

internal override bool IsHybridCacheActive() => _serviceProviderIsService?.IsService(typeof(HybridCache)) == true;

当应用同时使用HybridCache并把本包作为其二级(L2)分布式缓存时,连接会追加"HC"库名后缀,便于在 Redis 监控(如CLIENT LISTINFO)中区分流量来源。注册AddStackExchangeRedisCache本身并不自动引入HybridCache,两者是组合关系而非替代关系。

十、工程与测试佐证

  • 注册行为验证:CacheServiceExtensionsTests.cs 覆盖了“单例注册”“覆盖用户先前注册的 Scoped 实现”“链式调用返回同一 ServiceCollection”“无/有日志时均可解析IDistributedCache”等场景,其中RedisCache的可解析性得益于构造函数对ILogger的可选注入设计(内部构造器在无日志工厂时使用NullLogger)。
  • 过期语义验证:TimeExpirationTests.cs 与其异步版本 TimeExpirationAsyncTests.cs 验证绝对过期、秒级以下过期、负值报错等行为。
  • 读写删除验证:RedisCacheSetAndRemoveTests.cs 覆盖Set/Remove相关操作。
  • 测试基础设施:RedisTestConfig.cs 演示了如何以"localhost:6379"RedisPort = 6379)这样的连接串直接构造RedisCache(其CreateCacheInstance仅设置了ConfigurationInstanceName两个选项),也展示了测试进程自行拉起/连接 Redis 服务器的整套逻辑——这正是第五、六节所述两个最小配置项在真实场景中的直接应用。

十一、小结与建议

Microsoft.Extensions.Caching.StackExchangeRedis是 ASP.NET Core 生态中把 Redis 接入IDistributedCache抽象的标准通道。回顾核心要点:

  1. 注册极简AddStackExchangeRedisCache一行完成单例注册,连接串放入ConnectionStrings即可,示例见 PACKAGE.md。
  2. 隔离靠前缀:多应用共享 Redis 时务必设置互不相同的InstanceName
  3. 存储结构透明:每个键是一个包含absexp/sldexp/data三个字段的 Redis Hash,过期时间以 Ticks 存放、TTL 以秒下发。
  4. 健壮性内置AbortOnConnectFail被强制关闭保证启动可用;出现持续网络故障时可启用UseForceReconnectAppContext 开关获得 60 秒节流 + 30 秒错误窗口的强制重连兜底。
  5. 升级路径清晰:同一包内的RedisCache实现了支持零拷贝写入的IBufferDistributedCache,并可与HybridCache组合形成两级缓存,源码中均保留了相应挂点。

本文所有结论均可在仓库 src/Caching/StackExchangeRedis 目录下的源码、公共 API 清单与测试用例中得到验证,读者可结合这些文件进一步深入。

【免费下载链接】aspnetcoreASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.项目地址: https://gitcode.com/GitHub_Trending/as/aspnetcore

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询