Кэширование в ASP.NET Core. IMemoryCache

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

Идея кэширования

Идею использования кэширования в ASP.NET Core (и не только) можно продемонстрировать следующей схемой:

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

В каких случаях можно использовать кэширование? Кэширование рекомендуется использовать для данных, которые не меняются вообще (идеальный вариант) или изменяются редко. Например, не стоит использовать кэширование показаний таймера или результаты каких-либо вычислений, которые в любой момент могут измениться. При этом, если мы храним, например, аватары пользователей в БД, то имеет смысл загрузить их в кэш.

Типы кэшей

Можно выделить три типа кэшей, которые могут использовать приложения:

  1. Кэш в памяти (In-Memory Cache). Этот тип кэша используется в случае, если нам достаточно хранить данные в рамках одного процесса.  Когда процесс завершается, то кэш удаляется из памяти вместе с ним.
  2. Постоянный локальный кэш (Persistent in-process Cache) — это вариант, при котором создается резервная копию кэша вне памяти процесса, например, в файле или БД. В этом случае, при завершении процесса кэш не умирает и может быть восстановлен при следующем запуске процесса.
  3. Распределенный кэш (Distributed Cache) — такой кэш используется в том случае, если нужен общий кэш для нескольких машин. Распределенный кэш хранится в какой-либо внешней службе и, если один сервер сохранил элемент кэша, то другие серверы могут его использовать. Например, в ASP.NET Core для распределенного кэша может использоваться такой сервис, как Redis.

Сегодня мы рассмотрим первый тип кэша — In-Memory Cache или, как его ещё называют, локальный кэш.

Тестовое приложение

Прежде, чем перейдем непосредственно к работе с кэшем в приложении, создадим тестовый пример. Пусть в нашем приложении будет использоваться база с данными пользователей. Для работы будем использовать EF Core и SQlite. Вначале, создадим модель (класс) для хранения данных пользователя. Класс UserModel создадим в отдельном файле:

namespace AspCache
{
    public class UserModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
    }
}

Теперь создадим контекст БД. Для этого добавим в приложение новый файл с классом, назовем его AppUsersContext. Содержимое файла:

public class AppUsersContext: DbContext
 {
     public DbSet<UserModel> Users { get; set; }

     public AppUsersContext(DbContextOptions<AppUsersContext> options) : base(options) =>
     Database.EnsureCreated();
     protected override void OnModelCreating(ModelBuilder modelBuilder)
     {
         // инициализация БД начальными данными
         modelBuilder.Entity<UserModel>().HasData(
                 new UserModel { Id = 1, Name = "Tom", Email = "tom@mail.com" },
                 new UserModel { Id = 2, Name = "John", Email = "john@mail.com" },
                 new UserModel { Id = 3, Name = "Sam", Email = "sam@mail.com" },
                 new UserModel { Id = 4, Name = "Bob", Email = "bob@mail.com" }
         );
     }
 }

здесь мы сразу инициализируем БД начальными данными. Теперь создадим сервис для работы с пользователями:

namespace AspCache
{
    public class UserService
    {
        AppUsersContext _usersContext;
        public UserService(AppUsersContext context) 
        {
            _usersContext = context;
        }

        public UserModel GetUser(int id)
        {
            return _usersContext.Users.FirstOrDefault(x => x.Id == id);
        }
    }
}

Этот сервис использует созданный нами контекст данных и содержит всего один метод, возвращающий объект пользователя по его id или null, если пользователь не найден в БД. Теперь настроим наше приложение на работу с базой данных, подключим необходимые сервисы и создадим конечную точку приложения для получения данных пользователя:

namespace ASpCache
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            builder.Services.AddDbContext<AppUsersContext>(options => options.UseSqlite("Data Source=users.sqlite"));
            builder.Services.AddTransient<UserService>();

            var app = builder.Build();

            app.MapGet("/", () => "Hello ASP.NET Core Cache!");
            
            app.MapGet("/user/{id}", (int id, [FromServices] UserService data) => 
            {
                var _user = data.GetUser(id);
                if (_user == null) 
                {
                    return Results.Content($"Пользователь не найден");
                }
                return Results.Json(_user);
            });

            app.Run();
        }
    }
}

здесь мы подключаем в качестве сервиса наш контекст данных:

builder.Services.AddDbContext<AppUsersContext>(options => options.UseSqlite("Data Source=users.sqlite"));

добавляем сервис для работы с пользователями:

builder.Services.AddTransient<UserService>();

который используем в конечной точке:

app.MapGet("/user/{id}", (int id, [FromServices] UserService data) => 
{
    var _user = data.GetUser(id);
    if (_user == null) 
    {
        return Results.Content($"Пользователь не найден");
    }
    return Results.Json(_user);
});

Если пользователь будет найден в БД, то мы получим JSON объект с его данными. Посмотрим на работу приложения. Если пользователь не найден:

если пользователь найден:

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

Работа с IMemoryCache

Вначале приведем все изменения исходного кода приложения, а, затем рассмотрим его подробнее.

изменение в классе Program

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddDbContext<AppUsersContext>(options => options.UseSqlite("Data Source=users.sqlite"));
        builder.Services.AddTransient<UserService>();
        
        builder.Services.AddMemoryCache();//включаем кэширование
        
        var app = builder.Build();

        //код конечных точек

        app.Run();
    }
}

Здесь мы добавляем новый сервис с использованием метода расширения AddMemoryCache()

Сервис UserService:

public class UserService
{
    AppUsersContext _usersContext;
    ILogger _logger;
    IMemoryCache _cache;
    public UserService(AppUsersContext context, ILogger<UserService> logger, IMemoryCache memoryCache) 
    {
        _usersContext = context;
        _logger = logger;
        _cache = memoryCache;
    }

    public UserModel GetUser(int id)
    {
        UserModel user = null;
        _cache.TryGetValue(id, out user);
        if (user == null)  //не смогли получить данные из кэша
        {
            user = _usersContext.Users.FirstOrDefault(x => x.Id == id);
            if (user != null) //данные в БД есть
            {
                _cache.Set(id, user, new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(30)));
                _logger.LogInformation($"Пользователь {user.Name} помещен в кэш");
            }
        }
        else
        {
            _logger.LogInformation($"Пользователь {user.Name} был извлечен из кэша");
        }
        return user;
    }
}

Здесь изменений немного больше, а именно:

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

public UserService(AppUsersContext context, ILogger<UserService> logger, IMemoryCache memoryCache)

второе: в методе GetUser() мы вначале пробуем запросить данные из кэша для чего вызываем его метод TryGetValue().Если данные будут получены, то в лог выведется строка:

_logger.LogInformation($"Пользователь {user.Name} был извлечен из кэша");

если в кэше не окажется искомого элемента, то мы пробуем получить его из БД:

user = _usersContext.Users.FirstOrDefault(x => x.Id == id);

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

if (user != null) //данные в БД есть
{
    _cache.Set(id, user, new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(30)));
    _logger.LogInformation($"Пользователь {user.Name} помещен в кэш");
}

для записи в кэш мы используем метод Set() в который в качестве первого параметра передаем ключ по которому, в дальнейшем будет извлекаться элемент, в качестве второго параметра — значение элемента кэша и в третьем параметре — абсолютное время жизни кэша, которое в нашем случае будет равно 30 секундам.

Теперь запустим наше приложение и посмотрим на результат. Консоль приложения после первого запроса данных пользователя

Второй запрос того же пользователя:

Ждем 30+ секунд и пытаемся снова запросить данные пользователя:

При первом обращении данные пользователя выгружаются из БД и помещаются в кэш. Затем, в течение заданных нами 30 секунд элемент кэша живет и его можно получить из кэша. Через 30 секунд элемент удаляется из кэша и его снова необходимо читать из БД. Вот таким образом и работает кэширование в памяти в проектах ASP.NET Core. В следующей части мы разберемся с настройками кэширования и методами IMemoryCache более подробно.

Итого

Кэширование позволяет избежать выполнения дорогостоящих операций в тех случаях, когда данные не изменяются или же изменяются крайне редко. Самый простой тип кэширования — это кэширование в памяти (In-Memory Cache) при котором элементы кэша хранятся в памяти сервера и удаляются из неё как только завершается процесс, который этот кэш использует. В ASP.NET Core кэширование в памяти реализуется с использованием сервиса, который добавляется методом расширения AddMemoryCache() и реализует интерфейс IMemoryCache

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии