imemorycache不支持自动刷新,必须通过system.threading.timer等外部机制驱动主动重载。其过期策略仅触发被动淘汰,无法解决数据一致性窗口、首次访问延迟及多实例并发刷新等问题,需结合版本校验、分布式锁与错峰设计保障安全更新。

IMemoryCache 本身不支持自动刷新,所谓“自动刷新”必须靠外部定时器驱动 + 手动重载逻辑来实现。 它没有内置的 RefreshAfter 或类似 Redis 的 Expiration with background reload 机制。强行依赖 MemoryCacheEntryOptions.SlidingExpiration 或 AbsoluteExpirationRelativeToNow 只能触发被动淘汰,不能保证数据在过期前被主动更新。
为什么不能只靠 MemoryCacheEntryOptions 的过期设置
缓存项过期后,下一次 Get 才触发重建——这是应激装载(on-demand load),不是预热刷新。用户首次访问或缓存刚被淘汰时,会直连数据库/远程服务,出现明显延迟。对高并发、低延迟要求的接口,这不可接受。
-
SlidingExpiration适合“越用越活”的场景(如用户 session),不适合需要固定节奏更新的配置类缓存 -
AbsoluteExpirationRelativeToNow到点就删,但不会提前拉新数据;如果请求没来,缓存就空着,直到下次请求才重建 - 所有过期策略都不解决“数据源已变,但缓存还没变”的一致性窗口问题
用 System.Threading.Timer 驱动主动刷新(推荐)
这是轻量、可控、线程安全的主流做法,尤其适合 ASP.NET Core 中的 IMemoryCache。关键不是“让缓存自己动”,而是“定期调用你的刷新函数”。
- 在
IHostedService的StartAsync中初始化System.Threading.Timer,传入回调函数和初始延迟(如TimeSpan.Zero表示启动即刷第一次) - 回调函数里:用
TryGetValue检查缓存是否存在;若存在且业务上“该更新了”,就调用你的数据加载方法,再用Set覆盖旧值(注意传相同的MemoryCacheEntryOptions) - 务必包
try/catch:网络超时、DB 连接失败等异常不能让 Timer 停摆 - 别在回调里调
timer.Change():周期已由构造时的 third 参数(period)固定,手动改易出竞态 - 刷新逻辑尽量快:避免阻塞 Timer 回调线程池;耗时操作建议发到
Task.Run或用异步 API
刷新时如何判断“该不该更新”而不是盲目重拉
不是每次定时都无脑查库。要结合业务语义减少无效 IO:
- 加版本号或时间戳字段:缓存值里存一个
LastUpdatedUtc,刷新前先查数据库的MAX(ModifiedTime),仅当后者更新才重载 - 用分布式锁(如 Redis Lock)防多实例重复刷:如果你部署了多个 ASP.NET Core 实例,得确保同一时刻只有一个在执行刷新
- 错峰设计:不同缓存项设不同刷新间隔(如配置项每 5 分钟,统计报表每 30 分钟),避免秒级并发压 DB
- 失败降级:刷新失败时保留旧缓存,并记录 WARN 日志;不要清空或抛异常导致后续请求全打穿
真正的难点不在“怎么定时”,而在“怎么定义‘脏’”和“怎么安全替换”。很多团队卡在刷新逻辑没做幂等或没处理并发覆盖,结果缓存反而比不刷还乱。建议把刷新函数单独抽成可单元测试的方法,先验证数据一致性,再挂到 Timer 上。











