在开发应用程序时,缓存几乎是每个系统都会用到的一种性能优化手段。它的作用很直接:减少重复计算、减少数据库访问、降低系统延迟。
从 .NET Framework 4.0 开始,.NET 平台就内置了一个内存缓存组件 —— System.Runtime.Caching.MemoryCache。它属于标准库的一部分,不需要安装任何第三方包,就可以提供线程安全、策略灵活的缓存能力,非常适合大多数单机部署或中小规模应用的场景①。
内存缓存的核心价值
所谓内存缓存,本质上就是把那些经常使用但变化不频繁的数据提前存放在应用程序的内存里。
这样做的好处很明显:与访问数据库、读取文件或调用远程接口相比,从内存中读取数据的速度要快得多。在高并发场景下,这种差距会更加明显。合理使用缓存,可以显著降低接口延迟,同时减少数据库压力,提高系统整体吞吐量。
MemoryCache 提供了很多实用能力,例如:
-
过期策略:支持绝对过期(例如 5 分钟后失效)和滑动过期(只要被访问就会自动延长生命周期);
-
缓存依赖:可以监听文件、目录或其他缓存项的变化,一旦发生变化,缓存会自动失效;
-
移除回调:当缓存被清理时可以触发回调函数,用于记录日志或执行清理逻辑;
-
内存限制:可以设置缓存最大可用内存,避免无限增长导致应用程序占满内存②。
典型适用场景
缓存并不是万能的工具,只有在合适的场景中使用,才能真正发挥它的价值。
一般来说,内存缓存特别适合以下几类数据:
首先是变化不频繁的配置数据。例如系统配置、区域参数、字典表数据等,这类数据通常很少修改,但读取频率很高。
其次是计算成本很高的结果。例如复杂报表、统计分析、聚合计算等。如果每次请求都重新计算,系统开销会非常大,而缓存可以让这些计算只执行一次。
再者是热点查询数据。例如用户资料、商品详情等,这些数据访问频率很高,但更新并不频繁,非常适合缓存。
最后是短期中间状态数据。例如用户会话信息、流程审批状态等,这些数据生命周期短,但访问频率高③。
不过需要注意的是,如果数据要求强一致性(例如金融交易余额),或者系统是多实例部署(例如负载均衡下的多个服务节点),那么单机内存缓存就不太适合了。这类场景通常需要使用 Redis 等分布式缓存方案。
优势与局限
在实际项目中,MemoryCache 有不少优点。
首先,它是 .NET 标准库的一部分,无需额外安装组件,开箱即可使用。 其次,API 非常简单,核心操作只有 Add、Get 和 Remove。 另外,它是线程安全的,内部已经处理好并发访问问题。 同时,它的缓存策略非常灵活,可以组合使用过期策略、依赖监控和回调机制④。
当然,它也存在一些局限。
首先,它属于进程内缓存。应用程序一旦重启,缓存内容就会全部丢失。 其次,它无法跨服务器共享数据,因此在微服务或分布式系统中使用受到限制。 另外,如果没有设置合理的容量限制,缓存可能会不断增长,占用过多内存⑤。
实战示例
基础用法
下面是一个最基础的缓存示例:
using System;
using System.Runtime.Caching;
var cache = MemoryCache.Default;
var policy = new CacheItemPolicy
{
AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(5)
};
cache.Add("config", LoadConfig(), policy);
if (cache.Get("config") is Config config)
{
Console.WriteLine(config.Version);
}
cache.Remove("config");
这段代码将配置数据缓存 5 分钟,在缓存存在时直接读取,否则重新加载。
滑动过期与移除回调
在很多场景中,滑动过期会更合适。例如只要缓存一直被访问,就持续保留。
public string GetData(string key)
{
if (cache.Contains(key))
return cache.Get(key) asstring;
var data = ExpensiveCall(key);
var policy = new CacheItemPolicy
{
SlidingExpiration = TimeSpan.FromMinutes(10),
RemovedCallback = args =>
{
Console.WriteLine($"缓存 {args.CacheItem.Key} 因 {args.RemovedReason} 被移除");
}
};
cache.Add(key, data, policy);
return data;
}
在这个例子中,缓存只要在 10 分钟内被访问,就会自动延长生命周期。如果缓存被移除,还会触发回调记录日志。
文件依赖缓存
如果缓存数据来源于配置文件,可以通过文件监控机制实现自动失效:
var policy = new CacheItemPolicy();
policy.ChangeMonitors.Add(
new HostFileChangeMonitor(new[] { @"C:\\app\\config.json" })
);
cache.Add("appConfig", LoadConfig(), policy);
当 config.json 文件发生变化时,缓存会自动失效,下一次读取时重新加载数据。
高级技巧
内存与容量控制
可以通过 NameValueCollection 设置缓存内存限制:
var settings = new NameValueCollection
{
{ "cacheMemoryLimitMegabytes", "100" },
{ "physicalMemoryLimitPercentage", "50" }
};
var limitedCache = new MemoryCache("LimitedCache", settings);
这样可以限制缓存最多使用 100MB 内存,或不超过系统物理内存的 50%。
多命名缓存实例
在大型系统中,可以为不同模块创建独立缓存实例,避免键名冲突:
var userCache = new MemoryCache("UserCache");
var productCache = new MemoryCache("ProductCache");
userCache.Add("u_1001", user, policy);
productCache.Add("p_2001", product, policy);
这种方式可以让不同业务域拥有独立的缓存空间,管理更加清晰。
最佳实践建议
在实际项目中使用缓存时,有一些经验值得注意。
首先,不要缓存过大的对象。过大的对象可能进入大对象堆(LOH),增加垃圾回收压力。
其次,过期时间要合理设置。滑动过期适合热点数据,而绝对过期更适合有明确生命周期的数据。
第三,当数据库数据更新时,应主动清除对应缓存。否则系统可能长期读取旧数据。
另外,建议定期监控缓存命中率。例如通过 GetCount() 或应用监控工具,判断缓存是否真的带来了性能收益。
最后,尽量不要在大型系统中直接使用 MemoryCache.Default。因为它是全局共享实例,多个模块同时使用可能产生干扰。更好的做法是按业务创建独立的命名缓存实例⑥。
结语
MemoryCache 可以说是 .NET 开发者手边非常实用的一把小工具。它无需额外依赖,使用简单,而且性能可靠。在单机应用或小规模系统中,它足以应对大多数缓存需求。
当然,在复杂系统中,通常会结合 Redis 等分布式缓存方案,构建多级缓存架构。例如:本地内存缓存 + 分布式缓存 + 数据库。
真正关键的不是“有没有缓存”,而是是否根据业务特点设计了合理的缓存策略。只有在性能提升与资源消耗之间取得平衡,缓存才能发挥最大的价值。
参考资料
① Microsoft. MemoryCache Classhttps://learn.microsoft.com/en-us/dotnet/api/system.runtime.caching.memorycache
② Microsoft. CacheItemPolicy Classhttps://learn.microsoft.com/en-us/dotnet/api/system.runtime.caching.cacheitempolicy
③ Microsoft. Caching in .NET Applicationshttps://learn.microsoft.com/en-us/dotnet/core/extensions/caching
④ Andrew Lock. Using MemoryCache in ASP.NET Core
⑤ Ben Watson. Writing High-Performance .NET Code, 2nd ed., 2018
⑥ Microsoft. Caching Guidancehttps://learn.microsoft.com/en-us/azure/architecture/best-practices/caching




