我们将使用ASP.NET Core的内置内存缓存解决方案来缓存外部系统响应。(稍后我们可能会从内存中转换为IDistributedCache
。(
正如MSDN所建议的,我们希望使用Mircosoft.Extensions.Cacheching.memory的IMemoryCache
。
我们需要限制缓存的大小,因为默认情况下它是无限制的
因此,我创建了以下POC应用程序,以便在将其集成到我们的项目中之前对其进行一些处理。
我的自定义内存缓存以指定大小限制
public interface IThrottledCache
{
IMemoryCache Cache { get; }
}
public class ThrottledCache: IThrottledCache
{
private readonly MemoryCache cache;
public ThrottledCache()
{
cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = 2
});
}
public IMemoryCache Cache => cache;
}
将此实现注册为单例
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<IThrottledCache>(new ThrottledCache());
}
我创建了一个非常简单的控制器来处理这个缓存。
用于玩MemoryCache的沙盒控制器
[Route("api/[controller]")]
[ApiController]
public class MemoryController : ControllerBase
{
private readonly IMemoryCache cache;
public MemoryController(IThrottledCache cacheSource)
{
this.cache = cacheSource.Cache;
}
[HttpGet("{id}")]
public IActionResult Get(string id)
{
if (cache.TryGetValue(id, out var cachedEntry))
{
return Ok(cachedEntry);
}
else
{
var options = new MemoryCacheEntryOptions { Size = 1, SlidingExpiration = TimeSpan.FromMinutes(1) };
cache.Set(id, $"{id} - cached", options);
return Ok(id);
}
}
}
正如您所看到的,我的/api/memory/{id}
端点可以在两种模式下工作:
- 从缓存中检索数据
- 将数据存储到缓存中
我观察到以下奇怪的行为:
- GET
/api/memory/first
1.1(返回first
1.2(缓存项:first
- GET
/api/memory/first
2.1(返回first - cached
2.2(缓存项:first
- GET
/api/memory/second
3.1(返回second
3.2(缓存项:first
、second
- GET
/api/memory/second
4.1(返回second - cached
4.2(缓存项:first
、second
- GET
/api/memory/third
5.1(返回third
5.2(缓存项:first
、second
- GET
/api/memory/third
6.1(返回third
6.2(缓存项:second
、third
- GET
/api/memory/third
7.1(返回third - cached
7.2(缓存项:second
、third
正如您在第5个端点调用中看到的那样,这是我达到极限的地方。所以我的期望是:
- 缓存逐出策略删除
first
最旧的条目 - 缓存将
third
存储为最新
但这种期望的行为只发生在第6次呼叫时。
所以,我的问题是,当达到大小限制时,为什么我必须调用两次Set
才能将新数据放入MemoryCache?
编辑:添加定时相关信息以及
在测试过程中,整个请求流/链花费了大约15秒甚至更短的时间。
即使我将SlidingExpiration
更改为1小时,行为仍然完全相同。
我下载、构建并调试了Microsoft.Extension.Caching.Memory中的单元测试;似乎没有任何测试能够真正涵盖这个案例。
原因是:一旦你试图添加一个会使缓存超出容量的项目,MemoryCache就会在后台触发压缩。这将收回最旧的(MRU(缓存项,直到出现特定差异为止。在这种情况下,它试图删除总大小为1的缓存项,在您的情况下为"1";首先;,因为它是最后访问的。
但是,由于这个紧凑循环在后台运行,并且SetEntry()
方法中的代码已经在完整缓存的代码路径上,因此它将继续,而不将项添加到缓存中。
下一次尝试时,它成功了。
Repro:
class Program
{
private static MemoryCache _cache;
private static MemoryCacheEntryOptions _options;
static void Main(string[] args)
{
_cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = 2
});
_options = new MemoryCacheEntryOptions
{
Size = 1
};
_options.PostEvictionCallbacks.Add(new PostEvictionCallbackRegistration
{
EvictionCallback = (key, value, reason, state) =>
{
if (reason == EvictionReason.Capacity)
{
Console.WriteLine($"Evicting '{key}' for capacity");
}
}
});
Console.WriteLine(TestCache("first"));
Console.WriteLine(TestCache("second"));
Console.WriteLine(TestCache("third")); // starts compaction
Thread.Sleep(1000);
Console.WriteLine(TestCache("third"));
Console.WriteLine(TestCache("third")); // now from cache
}
private static object TestCache(string id)
{
if (_cache.TryGetValue(id, out var cachedEntry))
{
return cachedEntry;
}
_cache.Set(id, $"{id} - cached", _options);
return id;
}
}