当达到大小限制时,为什么需要在我的大小限制的MemoryCache上调用两次Set? [英] Why do I need to call twice the Set on my size limited MemoryCache when I hit the size limit?

查看:113
本文介绍了当达到大小限制时,为什么需要在我的大小限制的MemoryCache上调用两次Set?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

We are about to use the built-in in-memory cache solution of ASP.NET Core to cache aside external system responses. (We may shift from in-memory to IDistributedCache later.)
We want to use the Mircosoft.Extensions.Caching.Memory's IMemoryCache as the MSDN suggests.

我们需要限制缓存的大小,因为默认情况下它是无界的.
因此,在将其集成到我们的项目中之前,我已经创建了以下POC应用程序以对其进行处理.

We need to limit the size of the cache because by default it is unbounded.
So, I have created the following POC application to play with it a bit before integrating it into our project.

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

我创建了一个非常简单的控制器来处理此缓存.

I've created a really simple controller to play with this cache.

[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} 端点可以在两种模式下工作:

As you can see my /api/memory/{id} endpoint can work in two modes:

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

我观察到以下奇怪行为:

I have observed the following strange behaviour:

  1. 获取/api/memory/first
    1.1)返回 first
    1.2)缓存条目: first
  2. 获取/api/memory/first
    2.1)首先返回-缓存
    2.2)缓存条目: first
  3. 获取/api/memory/second
    3.1)返回 second
    3.2)缓存条目:第一第二
  4. 获取/api/memory/second
    4.1)返回 second-已缓存
    4.2)缓存条目:第一第二
  5. 获取/api/memory/third
    5.1)返回 third
    5.2)缓存条目:第一第二
  6. 获取/api/memory/third
    6.1)返回 third
    6.2)缓存条目:第二第三
  7. 获取/api/memory/third
    7.1)返回第三次-缓存
    7.2)缓存条目:第二第三
  1. GET /api/memory/first
    1.1) Returns first
    1.2) Cache entries: first
  2. GET /api/memory/first
    2.1) Returns first - cached
    2.2) Cache entries: first
  3. GET /api/memory/second
    3.1) Returns second
    3.2) Cache entries: first, second
  4. GET /api/memory/second
    4.1) Returns second - cached
    4.2) Cache entries: first, second
  5. GET /api/memory/third
    5.1) Returns third
    5.2) Cache entries: first, second
  6. GET /api/memory/third
    6.1) Returns third
    6.2) Cache entries: second, third
  7. GET /api/memory/third
    7.1) Returns third - cached
    7.2) Cache entries: second, third

您可以在第5个端点呼叫处看到达到极限的位置.所以我的期望是:

As you can see at the 5th endpoint call is where I hit the limit. So my expectation would be the following:

  • 缓存移出策略删除了 first 最旧的条目
  • 缓存将 third 作为最新存储
  • Cache eviction policy removes the first oldest entry
  • Cache stores the third as the newest

但是这种期望的行为只会在第6次通话时发生.

But this desired behaviour only happens at the 6th call.

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

So, my question is why do I have to call twice the Set in order to put new data into the MemoryCache when the size limit has reached?

编辑:还添加了与计时相关的信息

EDIT: Adding timing related information as well

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

During testing the whole request flow / chain took around 15 seconds or even less.

即使将 SlidingExpiration 更改为1小时,其行为也保持完全相同.

Even if I change the SlidingExpiration to 1 hour the behaviour remains exactly the same.

推荐答案

下载

I downloaded, built and debugged the unit tests in Microsoft.Extensions.Caching.Memory; there seems to be no test that seems that truly covers this case.

原因是:只要您尝试添加会使缓存超出容量的项目,

The cause is: as soon as you try to add an item which would make the cache go over capacity, MemoryCache triggers a compaction in the background. This will evict the oldest (MRU) cache entries up until a certain difference. In this case, it tries to remove a total size of 1 of cache items, in your case "first", because that was accessed last.

但是,由于此紧凑周期在后台运行,并且 SetEntry()方法中的代码为

However, since this compact cycle runs in the background, and the code in the SetEntry() method is already on the code path for a full cache, it continues without adding the item to the cache.

下次尝试时,它会成功.

The next time it tries to, it succeeds.

复制:

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

这篇关于当达到大小限制时,为什么需要在我的大小限制的MemoryCache上调用两次Set?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆