MemoryCache не принимает никакой продолжительности истечения срока действия

#c# #caching #memorycache

#c# #кэширование #memorycache

Вопрос:

У меня есть очень простой код, но то, что он делает, совершенно странно. Это простая абстракция кэша, которая выглядит следующим образом:

 public class CacheAbstraction
{
    private MemoryCache _cache;

    public CacheAbstraction()
    {
        _cache = new MemoryCache(new MemoryCacheOptions { });
    }

    public async Task<T> GetItemAsync<T>(TimeSpan duration, Func<Task<T>> factory, 
                                         [CallerMemberName] string identifier = null ) where T : class
    {
        return await _cache.GetOrCreateAsync<T>(identifier, async x =>
        {
            x.SetAbsoluteExpiration(DateTime.UtcNow.Add(duration));
            T result = null;
            result = await factory();
            return resu<
        });
    }
}
  

Теперь самое интересное: я передаю длительность истечения срока действия от 1 часа до 1d

Если я запускаю его в наборе тестов, все в порядке.

Если я запускаю его как приложение .net Core, срок действия всегда устанавливается на «now», и срок действия элемента истекает при следующей проверке кэша. ЧЕРТ возьми!?

Комментарии:

1. Я протестировал функцию (в течение нескольких часов), и она работает хорошо. По моему предположению, ваша проблема заключается в том, что вы используете функцию SetAbsoluteExpiration с параметром DateTimeOffset. Даже если вы отправите значение UTC, функция будет работать со смещением. Теперь, если у вас разные серверы, возможно, что смещения будут разными, и именно поэтому у вас такое странное поведение.

2. что я должен использовать вместо SetAbsoluteExpiration!? Я понимаю тему смещения UTC, но это объяснило бы пару часов и не установило бы все это на «сейчас». И не только в тестах: (

Ответ №1:

Я знаю, что прошло два года, но недавно я столкнулся с этой же проблемой (кажется, что срок действия элементов кэша истекает мгновенно) и нашел возможную причину. Две существенно недокументированные функции в MemoryCache : связанные записи кэша и распространение параметров.

Это позволяет дочернему объекту записи кэша пассивно распространять свои параметры вплоть до родительской записи кэша, когда дочерний объект выходит за пределы области видимости. Это делается через IDisposable , который ICacheEntry реализует и используется внутри MemoryCache в методах расширения, таких Set() как GetOrCreate/Async() и, ,. Это означает, что если у вас есть «вложенные» операции кэша, внутренние операции будут распространять свои параметры записи в кэше на внешние, включая токены отмены, обратные вызовы истечения срока действия и время истечения срока действия.

В моем случае мы использовали GetOrCreateAsync() и фабричный метод, который использовал библиотеку, которая выполняла собственное кэширование с использованием того же самого введенного IMemoryCache . Например:

 public async Task<Foo> GetFooAsync() {
    return await _cache.GetOrCreateAsync("cacheKey", async c => {
        c.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
        return await _library.DoSomething();
    });
}
  

Библиотека использует IMemoryCache внутренне (тот же экземпляр, введенный через DI) для кэширования результатов в течение нескольких секунд, по сути, делая это:

 _cache.Set(queryKey, queryResult, TimeSpan.FromSeconds(5));
  

Поскольку GetOrCreateAsync() это реализовано путем создания a CacheEntry внутри using блока, в результате 5-секундный срок действия, используемый библиотекой, распространяется вплоть до родительской записи кэша в GetFooAsync() , в результате чего объект Foo всегда кэшируется только в течение 5 секунд вместо 1 часа, эффективно завершая его немедленно.

Скрипта DotNet, показывающая это поведение: https://dotnetfiddle.net/fo14BT

Вы можете избежать такого поведения при распространении несколькими способами:

(1) Используйте TryGetValue() и Set() вместо GetOrCreateAsync()

 if (_cache.TryGetValue("cacheKey", out Foo result))
    return resu<

result = await _library.DoSomething();
return _cache.Set("cacheKey", result, TimeSpan.FromHours(1));
  

(2) Назначьте параметры записи в кэш после вызова другого кода, который также может использовать кэш

 return await _cache.GetOrCreateAsync("cacheKey", async c => {
    var result = await _library.DoSomething();
    // set expiration *after*
    c.AbsoluteExpiration = DateTime.Now.AddHours(1);
    return resu<
});
  

(и поскольку GetOrCreate/Async() не предотвращает повторный вход, эти два метода фактически одинаковы с точки зрения параллелизма).

Предупреждение: Даже тогда легко ошибиться. Если вы попытаетесь использовать AbsoluteExpirationRelativeToNow опцию (2), это не сработает, потому что установка этого свойства не удаляет AbsoluteExpiration значение, если оно существует, в результате чего оба свойства будут иметь значение в CacheEntry , и AbsoluteExpiration выполняется перед относительным.

На будущее Microsoft добавила функцию для управления этим поведением с помощью нового свойства MemoryCacheOptions.TrackLinkedCacheEntries , но она не будет доступна до .NET 7. Без этой будущей функции я не смог придумать способ для библиотек предотвратить распространение, кроме использования другого MemoryCache экземпляра.