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

Why do I need to call twice the Set on my size limited MemoryCache when I hit the size limit?

我们即将使用 ASP.NET Core 的内置内存缓存解决方案来缓存外部系统响应。 (稍后我们可能会从内存中转移到 IDistributedCache。)
我们要使用 Mircosoft.Extensions.Caching.Memory's IMemoryCache as the MSDN suggests.

我们需要限制缓存的大小,因为默认情况下它是无限的。
因此,在将其集成到我们的项目之前,我创建了以下 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;
}

将此实现注册为单例

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. 获取/api/memory/first
    1.1) Returns first
    1.2)缓存条目:first
  2. 获取/api/memory/first
    2.1) Returns first - cached
    2.2)缓存条目:first
  3. 获取/api/memory/second
    3.1) Returns second
    3.2)缓存条目:firstsecond
  4. 获取/api/memory/second
    4.1) Returns second - cached
    4.2)缓存条目:firstsecond
  5. 获取/api/memory/third
    5.1) Returns third
    5.2)缓存条目:firstsecond
  6. 获取/api/memory/third
    6.1) Returns third
    6.2)缓存条目:secondthird
  7. 获取/api/memory/third
    7.1) Returns third - cached
    7.2)缓存条目:secondthird

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

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

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


编辑:同时添加时间相关信息

在测试期间,整个请求流/链花费了大约 15 秒甚至更少。

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

downloaded, built and debugged the unit tests in Microsoft.Extensions.Caching.Memory;似乎没有真正涵盖这种情况的测试。

原因是:一旦您尝试添加一个会使缓存超出容量的项目,MemoryCache triggers a compaction in the background. This will evict the oldest (MRU) cache entries up until a certain difference。在这种情况下,它会尝试删除总大小为 1 的缓存项,在您的情况下是“第一个”,因为它是最后访问的。

但是,由于此紧凑循环在后台运行,并且 SetEntry() 方法中的代码是 already on the code path for a full cache,它会继续而不将项目添加到缓存。

下次尝试时,它会成功。

转载:

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;
    }
}