#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
экземпляра.