【问题标题】:Why do I need to call twice the Set on my size limited MemoryCache when I hit the size limit?为什么当我达到大小限制时,我需要在我的大小受限的 MemoryCache 上调用两次 Set?
【发布时间】:2020-08-10 15:01:06
【问题描述】:

我们即将使用 ASP.NET Core 的内置内存缓存解决方案来缓存外部系统响应。 (稍后我们可能会从内存转移到IDistributedCache。)
我们想使用Mircosoft.Extensions.Caching.MemoryIMemoryCache 作为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) 返回first
    1.2) 缓存条目:first
  2. 获取/api/memory/first
    2.1) 返回first - cached
    2.2) 缓存条目:first
  3. 获取/api/memory/second
    3.1) 返回second
    3.2) 缓存条目:firstsecond
  4. 获取/api/memory/second
    4.1) 返回second - cached
    4.2) 缓存条目:firstsecond
  5. 获取/api/memory/third
    5.1) 返回third
    5.2)缓存条目:firstsecond
  6. 获取/api/memory/third
    6.1) 返回third
    6.2) 缓存条目:secondthird
  7. 获取/api/memory/third
    7.1) 返回third - cached
    7.2) 缓存条目:secondthird

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

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

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

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


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

在测试整个请求流/链期间大约需要 15 秒甚至更少。

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

【问题讨论】:

  • 第三个不会添加到缓存中,缓存已满,有“第一”和“第二”。将“第一个”条目添加到缓存后一分钟后,它会过期并且可以添加新项目。至少,我猜它是这样工作的,去寻找文档......
  • 是的,请参阅副本:“如果缓存条目大小的总和超过 SizeLimit 指定的值,则不会缓存条目”。所以这不是您需要的第二次通话,而是两次通话之间的时间。
  • @CodeCaster 整个请求流大约需要 15 秒。如果我将 SlidingExpiration 更改为 1 小时,它的行为方式完全相同。
  • 我已经相应地更新了。
  • 如何观察缓存的条目?

标签: c# asp.net-core asp.net-core-3.1 memorycache


【解决方案1】:

downloadedbuilt 并调试了Microsoft.Extensions.Caching.Memory 中的单元测试;似乎没有真正涵盖这种情况的测试。

原因是:一旦您尝试添加一个会使缓存超出容量的项目,MemoryCache triggers a compaction 在后台。这将驱逐最旧的 (MRU) 缓存条目,直到 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;
    }
}

【讨论】:

  • 当我阅读following section 时,我想如果过期检查需要直接交互,那么驱逐也应该有。 (正如您所揭示的那样,压缩的触发器确实是SetEntry)。我天真的想法是驱逐是一个同步操作......我能以某种方式检测到容量溢出吗?这对我来说感觉有点奇怪// The entry was not added due to overcapacityentry.SetExpired(EvictionReason.Capacity); 例外情况可能在这里 IMO 更好
  • “我能以某种方式检测到容量溢出吗?” - 我不这么认为,除了使用反射。 “在 IMO 中出现异常可能会更好”- 在这种情况下,我认为将项目添加到缓存中不应该涉及异常。
  • 感谢您付出的所有努力。
  • 这应该添加到 MS 官方文档中,如果还没有的话。它帮助我了解了压缩和驱逐的工作原理。
猜你喜欢
  • 2019-01-21
  • 2013-11-29
  • 2011-06-18
  • 2016-12-13
  • 1970-01-01
  • 2017-03-12
  • 2022-11-28
  • 1970-01-01
  • 2014-01-24
相关资源
最近更新 更多