Кэширование — это относительно простая и, в тоже время, эффективная концепция в программировании, идея которой состоит в том, чтобы повторно использовать данные, не прибегая к выполнению повторных дорогостоящих операций. В ASP.NET Core предусмотрены различные механизмы кэширования данных.
Идея кэширования
Идею использования кэширования в ASP.NET Core (и не только) можно продемонстрировать следующей схемой:
Когда пользователь впервые запрашивает данные (верхний рисунок), то приложение делает запрос, например, к базе данных, извлекает необходимые данные, сохраняет результат в кэш и возвращает пользователю. Когда пользователь выполняет второй и последующие запросы, то приложение уже не обращаются к БД, а извлекает полученные ранее данные из кэша и возвращает их пользователю. Так как обращение к БД — это часто довольно трудоемкие операции, то использование кэша позволяет повысить производительность нашего приложения.
В каких случаях можно использовать кэширование? Кэширование рекомендуется использовать для данных, которые не меняются вообще (идеальный вариант) или изменяются редко. Например, не стоит использовать кэширование показаний таймера или результаты каких-либо вычислений, которые в любой момент могут измениться. При этом, если мы храним, например, аватары пользователей в БД, то имеет смысл загрузить их в кэш.
Типы кэшей
Можно выделить три типа кэшей, которые могут использовать приложения:
- Кэш в памяти (In-Memory Cache). Этот тип кэша используется в случае, если нам достаточно хранить данные в рамках одного процесса. Когда процесс завершается, то кэш удаляется из памяти вместе с ним.
- Постоянный локальный кэш (Persistent in-process Cache) — это вариант, при котором создается резервная копию кэша вне памяти процесса, например, в файле или БД. В этом случае, при завершении процесса кэш не умирает и может быть восстановлен при следующем запуске процесса.
- Распределенный кэш (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