В этом посте я описываю, как использовать MemoryCache и отслеживать время существования сущностей в приложениях .NET.
вступление
Кэширование — один из известных способов повысить производительность приложения и снизить нагрузку на поставщика данных. В .NET есть пакеты Microsoft.Extensions.Caching для использования разных хранилищ: Redis, SqlServer, Cosmos, Memory. Последний — самый простой способ. Он просто сохраняет данные в ОЗУ. Вы можете использовать его, просто установив Microsoft.Extensions.Caching.Memory из NuGet.
Использование кэша памяти
Для хранения данных используется внутренний ConcurrentDictionary
. Таким образом, операции set/get являются потокобезопасными. Взаимодействие с MemoryCache описано в IMemoryCache
. Этот интерфейс имеет несколько методов для установки, удаления и получения значения. Обычный алгоритм работы с кешем:
1. Попробуйте получить значение из кеша.
2. Если вы не получили значение из кеша, запросите источник данных и сохраните полученное значение в кеше.
Простой пример:
Мы должны быть осторожны в использовании памяти и избегать неконтролируемого распределения. Поэтому в приведенном выше коде мы ограничиваем размер кеша, время жизни сущностей на MemoryCacheEntryOptions
.
Отслеживание объектов
Одной из важных частей кэширования является обновление данных в хранилище кэша. В приведенном выше коде мы проверяем существование объекта во время получения значения. Если его нет, мы получаем значение от поставщика данных, а затем сохраняем его в кеше. Также для этого расширение IMemoryCache
имеет метод GetOrCreate<TItem>(Object key, Func<ICacheEntry,TItem> factory)
.
Следует упомянуть механизм истечения срока действия в MemoryCache
. Когда мы пытаемся получить значение из кеша. Последний получает значение из внутреннего словаря и проверяет, не истек ли срок его действия. Если да, сущность помечается как просроченная, а затем удаляется. Если нет, то кеш просто возвращает значение. В обоих случаях кеш запускает процесс аннулирования всех значений во внутреннем словаре в отдельном потоке. Период этой проверки вы можете настроить в ExpirationScanFrequency
поле MemoryCacheOptions
(по умолчанию 1 минута). Также в MemoryCache есть логика сжатия. Объекты удаляются в следующем порядке:
1. Все элементы с истекшим сроком действия.
2. Предметы по приоритету. Элементы с самым низким приоритетом удаляются первыми.
3. Объекты, которые использовались реже всего.
4. Предметы с самым ранним абсолютным сроком действия.
5. Предметы с самым ранним скользящим сроком действия.
Закрепленные элементы с приоритетом NeverRemove никогда не удаляются. По этим причинам нам приходится отслеживать значения в кеше и обновлять его. Самый простой способ — проверка значения методом TryGetValue
при получении его из кеша.
Проблема: обновление значения в MemoryCache
Мы видим в приведенном выше коде, что можно вызывать GetOrUpdate
из нескольких потоков, и значение кеша будет обновляться несколько раз в этих потоках — состояние гонки. Есть разные способы избежать этого:
- Использование примитивов синхронизации. Например, вы можете использовать
SemaphoreSlim
:
Также мы можем использовать Interlocked
методов типа здесь. Имеется замок под ключ. В любом случае, эта логика получения или обновления нарушает принцип единой ответственности. Вы получаете значение и иногда устанавливаете новое значение одним методом.
2. Использование Lazy<T>
подхода. Поместите экземпляр Lazy<T>
вместо T
в кеш. Этот способ описан в StackOverflow. Также есть реализация LazyCache. Ленивый подход лучше предыдущего. Это позволяет сохранить принцип единой ответственности и использовать экземпляр из кеша, когда это действительно необходимо.
3. Использование обратных вызовов. В официальной документации упоминаются обратные вызовы выселения. Давайте посмотрим, что это такое. Каждая сущность inMemoryCache
имеет коллекцию обратных вызовов типа PostEvictionCallbackRegistration
, которые вызываются после вытеснения значения из кеша в отдельном потоке. Вы можете добавить своего делегата методом RegisterPostEvictionCallback
в аргумент MemoryCacheEntryOptions
метода Set
:
Причина выселения устанавливается в EvictionReason.Removed
после удаления и EvictionReason.Expired
после проверки срока службы. Когда объект кеша был обновлен, причиной удаления будет EvictionReason.Replaced
В случае удаления из-за переполнения емкости, это EvictionReason.Capacity
Простой пример:
Он позволяет запускать фоновое обновление удаленных значений и разделять логику получения/установки — принцип единой ответственности. Но нужно помнить о состоянии гонки. CacheEntity
позволяет отслеживать изменения с помощью IChangeToken
. Я опускаю эту тему, так как она имеет аналогичный подход, и я не претендую на исчерпывающий учебник (определенно, это будет долгое чтение). Вы можете прочитать здесь об использовании токена изменения.
Краткое содержание
В этом посте я коснулся некоторых важных вопросов использования кеша памяти:
- Вы должны ограничить размер и время жизни сущностей. Это позволяет сократить использование памяти.
- Инвалидация кеша срабатывает при попытке получить значение. Он использует фоновый поток для проверки времени жизни и очистки объектов с истекшим сроком действия в кеше.
— MemoryCache является потокобезопасным, но не предотвращает состояние гонки для метода Set
. Поэтому я описал несколько подходов к решению этой проблемы. У каждого есть плюсы и минусы, выбирайте лучший для своего случая. Также я увидел интересный асинхронный подход на основе TaskCompletionsSource
. Дополнительную информацию вы можете получить из списка ниже.
P.S. Прочтите исходный код. Благодаря политике открытого исходного кода это хороший способ узнать больше, чем из документации.
- https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-6.0#use-setsize-size-and-sizelimit-to-limit-cache-size
- https://github.com/dotnet/runtime/tree/5c61ad424a7c0f50690f73235d695bad2c525634/src/libraries/Microsoft.Extensions.Caching.Memory
- https://blog.novanet.no/asp-net-core-memory-cache-is-get-or-create-thread-safe/
- https://cpratt.co/thread-safe-strong-typed-memory-caching-c-sharp/
- https://github.com/dotnet/runtime/issues/36500#issuecomment-491540894