Кэширование

Кэширование с помощью MemoryCache

Последнее обновление: 30.11.2022

Основы кэширования

Кэширование представляет собой сохранение данных в специальном месте для более быстрого доступа к ним в будущем. Применение кэширование может значительно повысить производительность приложения ASP.NET, существенно уменьшая количество обращений к источникам данных, например, к базам данных.

Когда надо кэшировать данные?

  • Когда данные являются внешними по отношению к приложению (например, приходят из базы данных или другого внешнего источника)

  • Когда данные не часто обновляются, относительно постоянны

  • Когда данные часто используются в приложении

При этом речь не идет о кэшировании всей таблицы базы данных или нескольких таблиц. Это может быть часть таблицы или срез данных, которые попадают под вышеописанные критерии.

Распространенные причины кэширования

  • Кэширование результатов запросов к бд, поскольку обращения к базе данных, как правило, являются узким местом приложения

  • Кэширование при высокой латентности сети. Когда приложение располагается в такой сетевой конфигурации, в которой некоторые аспекты сети замедляют работу приложения. (например, приложение располагается за файерволом, и валидация входящих и исходящих запросов занимает некоторое время). В этом случае кэш обычно располагается на другом хосте, где подобная латентность снижается к минимуму.

  • Кэширование для управления состоянием. Кэш может представлять некоторое общее состояние, которое могут использовать различные экземпляры приложений или части одного приложения.

Стратегии кэширования

  • Прекэширование (Pre-caching). Путем анализа разработчик определяет наиболее часто запрашиваемые или данные, которые могут сильно снизить производительность приложения. Подобные данные кэшируются при старте приложения. Затем после запуска приложения берет подобные данные из кэша вместо запрашивания из внешнего источника. Минус подобного подхода - необходимость синхронизации с внешним источником данных в случае их обновления, особенно когда приложение и база данных управляются разными командами разработчиков/разными компаниями

  • Кэширование по запросу (On-demand caching). Когда данные необходимы, приложение сначала обращается в кэш, если кэше найдены соответствующие данные, то они используются (это называется cache hit). Если данные в кэше отстуствуют (это называется cache miss), то приложение извлекает из базы данных и кэшируют для последующих запросов. Минус стратегии - необходимость делать запрос к бд, который снижает производительность приложения.

MemoryCache

Самым простым способом кэширования в ASP.NET Core предствляет использование объекта Microsoft.Extensions.Caching.Memory.IMemoryCache, который позволяет сохранять данные в кэше на сервере. Применяя методы интефейса IMemoryCache, мы можем управлять кэшем:

  • bool TryGetValue(object key, out object value): пытаемся получить элемент по ключу key. При успешном получении параметр value заполняется полученным элементом, а метод возвращает true

  • object Get(object key): дополнительный метод расширения, который получает по ключу key элемент и возвращает его

  • void Remove(object key): удаляет из кэша элемент по ключу key

  • object Set(object key, object value, MemoryCacheEntryOptions options): добавляет в кэш элемент с ключом key и значением value, применяя опции кэширования MemoryCacheEntryOptions

  • ICacheEntry CreateEntry(object key): добавляет в кэш или перезаписывает запись с ключом key. Возвращает новую запись

ASP.NET Core предоставляет встроенную реализацию интерфейса IMemoryCache - класс MemoryCache, который используется как реализация по умолчанию для сервиса IMemoryCache и который инкапсулирует все объекты кэша в виде словаря Dictionary.

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

Для простоты и демонстрации в качестве базы данных будем использовать базу данных SQLite, с которой будем работать через Entity Framework. Поэтому добавим в проект через Nuget пакет Microsoft.EntityFrameworkCore.Sqlite.

Далее определим в файле Program.cs следующий код приложения:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;

var builder = WebApplication.CreateBuilder(args);

// внедрение зависимости Entity Framework
builder.Services.AddDbContext<ApplicationContext>(options => options.UseSqlite("Data Source=usercacheapp.db"));
// внедрение зависимости UserService
builder.Services.AddTransient<UserService>();
// добавление кэширования
builder.Services.AddMemoryCache();
var app = builder.Build();

app.MapGet("/user/{id}", async (int id, UserService userService) =>
{
    User? user = await userService.GetUser(id);
    if (user != null) return $"User {user.Name}  Id={user.Id}  Age={user.Age}";
    return "User not found";
});
app.MapGet("/", () => "Hello World!");

app.Run();


public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public int Age { get; set; }
}
public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;
    public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options) => 
        Database.EnsureCreated();
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // инициализация БД начальными данными
        modelBuilder.Entity<User>().HasData(
                new User { Id = 1, Name = "Tom", Age = 23 },
                new User { Id = 2, Name = "Alice", Age = 26 },
                new User { Id = 3, Name = "Sam", Age = 28 }
        );
    }
}
public class UserService
{
    ApplicationContext db;
    IMemoryCache cache;
    public UserService(ApplicationContext context, IMemoryCache memoryCache)
    {
        db = context;
        cache = memoryCache;
    }
    public async Task<User?> GetUser(int id)
    {
        // пытаемся получить данные из кэша
        cache.TryGetValue(id, out User? user);
        // если данные не найдены в кэше
        if (user == null)
        {
            // обращаемся к базе данных
            user = await db.Users.FirstOrDefaultAsync(p => p.Id == id);
            // если пользователь найден, то добавляем в кэш - время кэширования 5 минут
            if (user != null)
            {
                Console.WriteLine($"{user.Name} извлечен из базы данных");
                cache.Set(user.Id, user, new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(5)));
            }
        }
        else
        {
            Console.WriteLine($"{user.Name} извлечен из кэша");
        }
        return user;
    }
}

Вкратце рассмотрим кода. Прежде всего определяем класс User, который будет описывать используемые данные и объекты которого будут храниться в базе данных SQLite:

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

Для взаимодействия с базой данных применяется класс контекста данных ApplicationContext:

public class ApplicationContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;
    public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options) => 
        Database.EnsureCreated();
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // инициализация БД начальными данными
        modelBuilder.Entity<User>().HasData(
                new User { Id = 1, Name = "Tom", Age = 23 },
                new User { Id = 2, Name = "Alice", Age = 26 },
                new User { Id = 3, Name = "Sam", Age = 28 }
        );
    }
}

Для тестирования в методе OnModelCreating() инициализируем базу данных тремя объектами.

Для взаимодействия с контекстом данных и кэшем определен класс UserService:

public class UserService
{
    ApplicationContext db;
    IMemoryCache cache;
    public UserService(ApplicationContext context, IMemoryCache memoryCache)
    {
        db = context;
        cache = memoryCache;
    }
    public async Task<User?> GetUser(int id)
    {
        // пытаемся получить данные из кэша
        cache.TryGetValue(id, out User? user);
        // если данные не найдены в кэше
        if (user == null)
        {
            // обращаемся к базе данных
            user = await db.Users.FindAsync(id);
            // если пользователь найден, то добавляем в кэш - время кэширования 5 минут
            if (user != null)
            {
                Console.WriteLine($"{user.Name} извлечен из базы данных");
                cache.Set(user.Id, user, new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(5)));
            }
        }
        else
        {
            Console.WriteLine($"{user.Name} извлечен из кэша");
        }
        return user;
    }
}

Данный сервис через встроенный механизм внедрения зависимостей будет получать контекст данных и использовать его для взаимодействия с бд. Кроме того, данный класс реализует логику кэширования - также через механизм внедрения зависимостей в конструкторе мы можем получить объект кэша IMemoryCache.

В методе GetUser() сервис UserService получает объект User по id. При получении объекта вначале пытаемся найти этот объект в кэше:

cache.TryGetValue(id, out User? user);

Здесь ключами элементов в кэше являются значения id, а значения элементов - объекты User. Если ключ в кэше был найден, то в объект user передается извлекаемое из кэша значение, а метод TryGetValue() возвращает true

Если в кэше не оказалось объекта, то извлекаем его и бд и затем добавляем в кэш.

user = await db.Users.FindAsync(id);
if (user != null)
{
    Console.WriteLine($"{user.Name} извлечен из базы данных");
    cache.Set(user.Id, user, new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(5)));
}

Для добавления в кэш в метод Set() передаем ключ объекта - его Id, затем передаем само кэшируемое значение - извлеченный из БД объект User. И в конце для установки времени кэширования применяется метод SetAbsoluteExpiration объекта MemoryCacheEntryOptions, который в данном случае таже устанавливает 5 минут.

Регистрация IMemoryCache

Чтобы получить сервис IMemoryCache в приложении его необходимо добавить в коллекцию сервисов с помощью вызова:

builder.Services.AddMemoryCache();

По сути этот сервис устанавливает зависимость для IMemoryCache, создавая объект синглтон:

builder.Services.TryAdd(ServiceDescriptor.Singleton<IMemoryCache, MemoryCache>());

И для тестирования определим конечную точку, где клиент передает id через параметр маршрута, и сервис UserService по этому id пытается найти в базе данных и кэше нужный объект User:

app.MapGet("/user/{id}", async (int id, UserService userService) =>
{
    User? user = await userService.GetUser(id);
    if (user != null) return $"User {user.Name}  Id={user.Id}  Age={user.Age}";
    return "User not found";
});

Запустим приложение и обратимся по адресу https://localhost:xxxx/user/1 (то есть для получения объекта User с id=1). В итоге при первом обращении к приложению данные будут извлекаться из базы данных и сохраняться в кэш. При всех последующих обращениях в пределах времени кэширования (в данном случае в течение 5 минут) данные будут извлекаться из кэша:

IMemoryCache и кэширование в приложении ASP.NET Core на C#

MemoryCacheEntryOptions

Для установки параметров кэширования в метод Set() в качестве третьего параметра передается объект MemoryCacheEntryOptions, который устанавливает настройки кэширования объекта с помощью ряда свойств:

  • AbsoluteExpiration: возвращает или задает абсолютную дату окончания кэширования

  • AbsoluteExpirationRelativeToNow: возвращает или задает абсолютную дату окончания кэширования относительно текущего момента

  • ExpirationTokens: возвращает токены в виде объектов IChangeToken, которые приводят к истечению срока действия записи в кэше.

  • PostEvictionCallbacks: возвращает или задает колбеки, которые вызываются после удаления записи из кэша.

  • Priority: возвращает или задает приоритет сохранения записи в кэше во время очистки, активируемой при нехватке памяти. Представляет одно из значений перечисления CacheItemPriority. Значение по умолчанию — Normal. Другие значения - High, Low и NeverRemove

  • Size: возвращает или задает размер значения записи в кэше.

  • SlidingExpiration: возвращает или задает время, в течение которого запись кэша может быть неактивной (то есть к ней нет обращений), прежде чем она будет удалена.

  • Это значение не увеличивает время существования записи сверх абсолютного срока действия (если он задан).

Применим ряд этих свойств. Для этого изменим в классе UserService следующим образом:

public async Task<User?> GetUser(int id)
{
    // пытаемся получить данные из кэша
    cache.TryGetValue(id, out User? user);
        
    // если данные не найдены в кэше
    if (user == null)
    {
        // обращаемся к базе данных
        user = await db.Users.FindAsync(id);
        // если пользователь найден, то добавляем в кэш
        if (user != null)
        {
            Console.WriteLine($"{user.Name} извлечен из базы данных");

            // определяем параметры кэширования
            var cacheOptions = new MemoryCacheEntryOptions()
            {
                // кэширование в течение 1 минуты
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1),
                // низкий приоритет
                Priority = 0,
            };
            // определяем коллбек при удалении записи из кэша
            var callbackRegistration = new PostEvictionCallbackRegistration();
            callbackRegistration.EvictionCallback =
                (object key, object? value, EvictionReason reason, object? state) => Console.WriteLine($"запись {id} устарела");
            cacheOptions.PostEvictionCallbacks.Add( callbackRegistration );

            cache.Set(user.Id, user, cacheOptions);
        }
    }
    else
    {
        Console.WriteLine($"{user.Name} извлечен из кэша");
    }
    return user;
}

Стоит отметить, что коллбек вызывается не сразу после окончания срока кэширования записи, а при первом после завершении кэширования обращении к кэшу

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850