在分布式系统里,缓存几乎是绕不开的一环。.NET Core提供了IDistributedCache这个统一的分布式缓存抽象接口,开发者可以基于它使用内存、SQL Server或者Redis等不同的后端实现,而不需要改动业务代码。本文围绕Redis这一最常用的缓存后端,从官方包的快速接入、自定义实现类的编写,到生产环境的性能优化,完整讲解集成过程中的关键细节。

一、官方RedisCache包的快速接入
微软官方提供了Microsoft.Extensions.Caching.StackExchangeRedis包,它内部基于著名的StackExchange.Redis客户端实现了IDistributedCache接口。接入的第一步是安装NuGet包,可以在项目目录下执行如下命令,也可以通过Visual Studio的包管理器搜索安装。
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
安装完成后,在Program.cs或Startup.cs中注册服务即可。注册时需要提供Redis连接字符串,格式遵循StackExchange.Redis的约定,例如包含主机地址、端口、密码等参数。下面的代码展示了最小化的注册方式。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
// 给缓存实例起个名字,避免多套系统共用Redis时key冲突
options.InstanceName = "MyApp:";
});
builder.Services.AddControllers();
var app = builder.Build();连接字符串可以写在appsettings.json里,典型格式形如localhost:6379,password=xxx,defaultDatabase=0。注册完成后,就可以在任意类中通过构造函数注入IDistributedCache来使用缓存了。需要注意的是,这个接口提供的方法都是基于字节数组的,存取字符串或对象时需要自己做序列化,这一点后文会详细展开。
public class ProductService
{
private readonly IDistributedCache _cache;
public ProductService(IDistributedCache cache)
{
_cache = cache;
}
public async Task<string> GetProductInfoAsync(string productId)
{
var key = $"product:{productId}";
var cached = await _cache.GetStringAsync(key);
if (!string.IsNullOrEmpty(cached))
{
return cached;
}
// 模拟从数据库读取
var data = $"商品{productId}的详细信息";
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
};
await _cache.SetStringAsync(key, data, options);
return data;
}
}二、理解IDistributedCache接口的设计与过期策略
IDistributedCache接口定义了Get、Set、Refresh、Remove四个核心操作,每个操作都有同步和异步两个版本。Set方法接受一个DistributedCacheEntryOptions参数,通过它可以控制缓存的过期行为。理解这两种过期机制的区别,是正确使用缓存的前提。
第一种是绝对过期,也就是AbsoluteExpiration或AbsoluteExpirationRelativeToNow,缓存到达指定时间点后立即失效。第二种是滑动过期,对应SlidingExpiration属性,只要缓存在这段时间内被访问过,过期时间就会不断顺延,只有连续一段时间没人访问才会真正失效。两种方式可以组合使用,常见的做法是设置一个较长的绝对过期时间,再配合一个较短的滑动过期时间,这样既能保证热点数据持续命中,又能避免冷数据长期占用内存。
特别提醒:如果只设置SlidingExpiration而不设置绝对过期,某些永远不会被淘汰的访问模式可能导致缓存数据长期不更新,建议总是搭配绝对过期一起使用。
关于接口的底层实现,官方RedisCache包实际上是把缓存的过期时间编码进了Redis的key本身。因为Redis原生的过期精度是秒级,而.NET侧支持毫秒级的滑动过期,所以实现时会把绝对过期时间戳和滑动过期间隔拼接在key的末尾,读取时再解析出来计算剩余有效期。了解这个细节有助于排查一些奇怪的现象,比如在Redis里直接用KEYS命令查看时会发现key比预期长了一截,这属于正常行为,不建议手工去拼接或修改这些key。
三、封装泛型扩展,解决序列化痛点
原生的IDistributedCache接口只支持byte数组,每次存取都要手动序列化反序列化,写起来非常啰嗦。实践中通常会在它之上封装一层泛型扩展方法,用JSON作为默认的序列化格式,让业务代码可以直接读写对象。
public static class DistributedCacheExtensions
{
public static async Task SetObjectAsync<T>(
this IDistributedCache cache,
string key,
T value,
DistributedCacheEntryOptions options)
{
var json = JsonSerializer.Serialize(value);
await cache.SetStringAsync(key, json, options);
}
public static async Task<T?> GetObjectAsync<T>(
this IDistributedCache cache,
string key)
{
var json = await cache.GetStringAsync(key);
return string.IsNullOrEmpty(json)
? default
: JsonSerializer.Deserialize<T>(json);
}
}封装之后业务代码会简洁很多,直接调用cache.GetObjectAsync<Product>(key)就能拿到对象。如果对性能要求更高,可以把序列化器换成System.Text.Json的源生成模式,或者使用MessagePack、Protobuf这类二进制协议,它们在序列化速度和体积上都明显优于JSON。不过要注意,二进制格式可读性差,排查问题时不如JSON直观,需要根据团队情况权衡。
另外还有一点值得强调:缓存的key设计同样重要。建议采用统一的命名规范,比如用业务域加实体加ID的形式,配合注册时的InstanceName前缀,可以让不同应用共享同一个Redis实例而互不干扰。乱起key名在小型项目里看不出问题,一旦系统多了之后,清理缓存、排查热点key都会变得非常痛苦。
四、生产环境优化:连接管理、异常降级与缓存穿透
StackExchange.Redis底层维护了一个基于多路复用的单连接,理论上不需要手动创建连接池,但要特别警惕在代码中反复new ConnectionMultiplexer的做法,这会消耗大量Socket资源并触发连接风暴。正确方式是把ConnectionMultiplexer注册为单例,通过服务容器注入使用。注册时可以借助AddSingleton把连接对象和IDistributedCache一起暴露出来。
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(
builder.Configuration.GetConnectionString("Redis")!));
builder.Services.AddStackExchangeRedisCache(options =>
{
options.ConnectionMultiplexerFactory = async () =>
await Task.FromResult(
ConnectionMultiplexer.Connect(
builder.Configuration.GetConnectionString("Redis")!));
});其次是异常降级。Redis一旦不可用,如果缓存层直接抛异常,整个请求链路都会被拖垮,这比没有缓存还糟糕。稳妥的做法是在访问缓存的代码外面套一层try-catch,Redis出问题时记一条日志然后回源数据库,保证核心业务不中断。可以把这个逻辑统一收敛到前面封装的扩展方法里,避免每个业务点都写一遍。
最后聊聊缓存穿透和击穿的防护。查询一个不存在的数据时,缓存永远不可能命中,请求会持续打到数据库上,这就是典型的穿透问题。常用手段是把空结果也缓存一小段时间,比如三十秒,或者用布隆过滤器在入口处拦截明显不存在的key。至于热点key失效瞬间的击穿问题,可以引入简单的互斥逻辑,只允许一个请求回源重建缓存,其他请求短暂等待后重新读取。这些策略与IDistributedCache本身的集成并不冲突,都属于在其之上构建的业务层防护,组合起来才能让Redis缓存层在真正的高并发场景下站稳脚跟。