在单体应用时代,我们习惯用MemoryCache把数据存在进程内存里,简单又高效。可一旦应用扩展到多实例部署,问题就来了:每个实例各存一份缓存,数据不一致、内存重复占用、某台机器重启缓存全丢。这时候就需要把缓存抽出来放到独立的缓存服务中,Redis正是这个场景下最常见的选择。.NET Core提供了IDistributedCache这个统一抽象,配合Redis客户端,几行配置就能接入,而且未来如果换成本地内存缓存或其他缓存服务,业务代码几乎不用改。

一、IDistributedCache是什么,为什么要用它
IDistributedCache是.NET Core中定义在Microsoft.Extensions.Caching.Abstractions包里的一个接口,它抽象了“分布式缓存”这个概念,屏蔽了底层实现细节。接口本身非常精简,核心方法就几个:GetAsync、SetAsync、RemoveAsync、RefreshAsync,分别对应读取、写入、删除和刷新过期时间。
它的价值在于解耦。你今天的项目用Redis,明天的项目要用Memcached或者SQL Server做缓存,业务代码里依赖的始终是IDistributedCache接口,只需要换掉ConfigureServices里的注册代码即可。这种面向接口的编程方式,让缓存的替换成本降到最低。
需要注意的是,IDistributedCache操作的数据是字节数组和字符串,不是强类型对象。所以存取对象时需要自己做序列化,这一点后面会讲怎么封装。
二、安装与注册Redis缓存服务
第一步是安装官方封装包。在包管理控制台执行:
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
这个包底层依赖StackExchange.Redis客户端,并把它适配成了IDistributedCache的实现。如果你需要更细粒度的控制,比如连接池、哨兵模式配置,也可以直接引入StackExchange.Redis,自己写适配类,但对大多数场景,官方封装已经够用。
注册服务只需要在Program.cs(.NET 6及以上)中一行代码:
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "MyApp:";
});其中Configuration填Redis的连接字符串,例如127.0.0.1:6379,密码认证写成127.0.0.1:6379,password=你的密码。InstanceName是可选的前缀,所有通过这个实例写入的键都会自动带上这个前缀,相当于命名空间,多系统共用一个Redis实例时特别有用,能有效避免键名冲突。
注册完成后,在任何地方都可以通过构造函数注入IDistributedCache来使用它。
三、基本的读写操作示例
下面写一个典型的“先查缓存,缓存没有再查数据库并回填”的服务类:
public class ProductService
{
private readonly IDistributedCache _cache;
private readonly ProductRepository _repository;
public ProductService(IDistributedCache cache, ProductRepository repository)
{
_cache = cache;
_repository = repository;
}
public async Task<Product?> GetProductAsync(int id)
{
string key = $"product:{id}";
string? cached = await _cache.GetStringAsync(key);
if (cached != null)
{
return JsonSerializer.Deserialize<Product>(cached);
}
Product? product = await _repository.GetByIdAsync(id);
if (product != null)
{
// 设置两小时过期,绝对过期时间
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
};
await _cache.SetStringAsync(key, JsonSerializer.Serialize(product), options);
}
return product;
}
}这段代码体现了缓存的经典模式:读缓存、未命中查库、回填缓存。GetStringAsync和SetStringAsync是对字节数组操作的封装,内部就是UTF-8编码转换,日常用字符串方法就够了,因为JSON序列化后的对象本身也是字符串。
DistributedCacheEntryOptions支持三种过期策略:绝对过期、滑动过期以及两者组合。绝对过期指到达固定时间点后失效;滑动过期指每次访问都会重置倒计时,适合“经常被访问就保留,没人访问就淘汰”的场景。两者组合时,滑动过期的重置不会超过绝对过期的上限,这是比较稳妥的配置。
四、封装扩展方法,告别重复的序列化代码
每个业务方法里都写一遍序列化和反序列化很啰嗦,可以写一组扩展方法统一处理:
public static class DistributedCacheExtensions
{
public static async Task<T?> GetObjectAsync<T>(
this IDistributedCache cache, string key)
{
string? json = await cache.GetStringAsync(key);
return json is null ? default : JsonSerializer.Deserialize<T>(json);
}
public static async Task SetObjectAsync<T>(
this IDistributedCache cache, string key, T value,
TimeSpan? expiration = null)
{
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromMinutes(30)
};
await cache.SetStringAsync(key, JsonSerializer.Serialize(value), options);
}
}封装之后业务代码变得非常干净:await _cache.GetObjectAsync<Product>($"product:{id}")一行就能拿到反序列化好的对象。如果对性能有更高要求,可以把JSON换成MessagePack,字节数组体积更小、序列化更快,代价是可读性下降,排查问题时不能直接在Redis里看到内容。
五、生产环境必须注意的几个问题
首先是缓存键的命名。建议统一采用业务模块:实体:标识的格式,比如order:summary:10086,配合InstanceName前缀,层次清晰,排查和批量删除都方便。Redis没有按模式删除的原子命令,规范的键名是后期运维的基础。
其次是缓存穿透问题。当查询一个根本不存在的ID时,每次请求都会穿过缓存打到数据库,恶意请求会拖垮后端。常见做法是对空结果也做缓存,但过期时间设得短一些,比如30秒到5分钟,这样既挡住了穿透,又不会长期占用内存。缓存雪崩则是指大量键在同一时刻集中过期,请求瞬间全落到数据库上,应对办法是给过期时间加上随机偏移,例如基准两小时再随机加减10分钟,把失效时间打散。
最后是连接健康检查。Redis连接断开时,StackExchange.Redis会自动重连,但操作会短暂失败。关键业务建议对缓存读写加上try-catch或Polly重试策略,缓存失败时降级直接查数据库,而不是让整个请求失败。记住一条原则:缓存是加速手段,不是必需依赖,任何缓存故障都不应该导致服务不可用。在Microsoft.Extensions.Caching.StackExchangeRedis之上,还可以结合HybridCache这类两级缓存方案(内存加分布式)进一步降低网络开销,追求极致性能的项目值得考虑。
总的来说,IDistributedCache加上Redis的组合,是.NET Core生态里最主流的分布式缓存方案,接入成本低、抽象程度合适、扩展性好。把序列化封装、键名规范、过期策略和降级逻辑这几件事做好,这套方案完全能撑起中大型项目的缓存需求。
IDistributedCacheRedis分布式缓存.NET Core缓存修改时间:2026-09-09 19:23:02