Кэширование — это относительно простая и, в тоже время, эффективная концепция в программировании, идея которой состоит в том, чтобы повторно использовать данные, не прибегая к выполнению повторных дорогостоящих операций. В 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


