当我达到大小限制时,为什么我需要调用大小限制MemoryCache上的Set两倍



我们将使用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}端点可以在两种模式下工作:

  • 从缓存中检索数据
  • 将数据存储到缓存中

我观察到以下奇怪的行为:

  1. GET/api/memory/first
    1.1(返回first
    1.2(缓存项:first
  2. GET/api/memory/first
    2.1(返回first - cached
    2.2(缓存项:first
  3. GET/api/memory/second
    3.1(返回second
    3.2(缓存项:firstsecond
  4. GET/api/memory/second
    4.1(返回second - cached
    4.2(缓存项:firstsecond
  5. GET/api/memory/third
    5.1(返回third
    5.2(缓存项:firstsecond
  6. GET/api/memory/third
    6.1(返回third
    6.2(缓存项:secondthird
  7. GET/api/memory/third
    7.1(返回third - cached
    7.2(缓存项:secondthird

正如您在第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;
}
}

最新更新