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

Pet*_*ala 4 c# memorycache asp.net-core asp.net-core-3.1

我们即将使用 ASP.NET Core 的内置内存缓存解决方案来缓存外部系统响应。(我们可能会从内存中转移到IDistributedCache以后。)
我们想使用Mircosoft.Extensions.Caching.Memory的,IMemoryCache正如MSDN 建议的那样

我们需要限制缓存的大小,因为默认情况下它是无界的。
因此,在将它集成到我们的项目中之前,我创建了以下 POC 应用程序来使用它。

我的自定义 MemoryCache 以指定大小限制

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;
}
Run Code Online (Sandbox Code Playgroud)

将此实现注册为单例

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddSingleton<IThrottledCache>(new ThrottledCache());
}
Run Code Online (Sandbox Code Playgroud)

我已经创建了一个非常简单的控制器来使用这个缓存。

用于玩 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,我的/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) 缓存条目:first,second
  4. GET /api/memory/second
    4.1) 返回second - cached
    4.2) 缓存条目:first,second
  5. GET /api/memory/third
    5.1) 返回third
    5.2) 缓存条目:first,second
  6. GET /api/memory/third
    6.1) 返回third
    6.2) 缓存条目:second,third
  7. GET /api/memory/third
    7.1) 返回third - cached
    7.2) 缓存条目:second,third

正如您在第 5 个端点调用中看到的那样,我达到了限制。所以我的期望如下:

  • 缓存逐出策略删除first最旧的条目
  • 缓存存储third为最新的

但是这种期望的行为只发生在第 6 次调用时。

所以,我的问题是,Set当达到大小限制时,为什么我必须调用两次才能将新数据放入 MemoryCache?


编辑:也添加时序相关信息

在测试过程中,整个请求流/链花费了大约 15 秒或更短的时间。

即使我将其更改SlidingExpiration为 1 小时,行为仍然完全相同。

Cod*_*ter 5

我在Microsoft.Extensions.Caching.Memory 中下载构建和调试单元测试;似乎没有真正涵盖这种情况的测试。

原因是:一旦您尝试添加一个会使缓存超出容量的项目,MemoryCache 就会在后台触发压缩。这将驱逐最旧的 (MRU) 缓存条目,直到出现一定差异。在这种情况下,它会尝试删除总大小为 1 的缓存项,在您的情况下是“第一个”,因为它是最后访问的。

但是,由于这个紧凑循环在后台运行,并且SetEntry()方法中的代码已经在完整缓存的代码路径上,因此它会继续而不将项目添加到缓存中。

下次它尝试时,它成功了。

复述:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)