Содержание
Распределенное кэширование (Distributed Cache) удобно использовать в том случае, если нужен общий кэш для нескольких машин. Распределенный кэш хранится в какой-либо внешней службе и, если один сервер сохранил элемент кэша, то другие серверы могут его использовать. Например, в ASP.NET Core для распределенного кэша может использоваться такой сервис, как Redis.
В любом случае, какую бы службу или сервис мы не использовали в качестве распределенного кэша будь то Redis или, как в случае с сессиями в ASP.NET Core, распределенный кэш в памяти, мы будем использовать одну из реализаций интерфейса IDistributedCache
. Для управления распределенным кэшем этот тип предоставляет различные методы, основные из которых следующие:
Метод | Описание |
Get(string key) |
возвращает значение с указанным ключом. Значение возвращается в виде массива байтов byte[]? |
Refresh(string key) |
обновляет значение в кэше по ключу, сбрасывая его срок кэширования |
Remove(string key) |
удаляет значение с указанным ключом. |
Set(string key, byte[] value, DistributedCacheEntryOptions options) |
устанавливает значение с указанным ключом. В качестве значения выступает массив байтов |
SetString(string key, string value) |
устанавливает значение с указанным ключом. В качестве значения выступает строка |
GetString(string key) |
возвращает значение с указанным ключом. Значение возвращается в виде строки |
Для каждого метода из таблицы также предусмотрена его асинхронная версия, например GetAsync()
, SetAsync()
и т.д. Как можно видеть из таблицы, при работе с распределенным кэшем мы можем оперировать строками или массивами байтов, например, если мы хотим закэшировать файл.
Рассмотрим использование кэширования на примере Redis.
Установка Redis на Windows
Для установки Redis для Windows необходимо скачать установочный файл с GitHub. Сама установка простая. Максимум, что вам предложат — это установить порт
и максимальный размер
После установки можно перейти в папку установки Redis и запустить сервер:
Вы должны увидеть следующее сообщение:
Теперь мы можем подключиться к Redis и использовать его в качестве распределенного кэша.
Подключение к Redis из приложения ASP.NET Core
Для подключения к Redis из приложения ASP.NET Core необходимо установить nuget-пакет Microsoft.Extensions.Caching.StackExchangeRedis. Если Redis работает на том же компьютере, что и ваше приложение, то в приложении настройка сервиса кэширования с использованием Redis будет выглядеть следующим образом:
// добавление кэширования builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = "localhost"; options.InstanceName = "local"; });
Метод AddStackExchangeRedisCache()
в качестве параметра принимает делегат Action<Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions>
. Объект RedisCacheOptions
позволяет установить настройки подключения с помощью свойств:
Configuration
— конфигурация подключения к серверу Redis. Для подключения к внешним серверам Redis необходимо указывать адрес и порт.InstanceName
— имя экземпляра Redis. Это имя будет использоваться в качестве префикса для сохраняемых в кэше данных.
Тестовое приложение для работы с распределенным кэшем
Структура проекта:
Ниже приведен исходный код приложения, использующего кэширование. Класс Program
:
using Microsoft.EntityFrameworkCore; namespace AspRedisCache { 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.AddStackExchangeRedisCache(options => { options.Configuration = "localhost"; options.InstanceName = "local"; }); 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(); } } }
Класс UserService
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using System.Text.Json; namespace AspRedisCache { public class UserService { AppUsersContext _usersContext; ILogger _logger; IDistributedCache _cache; public UserService(AppUsersContext context, ILogger<UserService> logger, IDistributedCache memoryCache) { _usersContext = context; _logger = logger; _cache = memoryCache; } public UserModel GetUser(int id) { UserModel user = null; var userJson = _cache.GetString(id.ToString()); if (string.IsNullOrEmpty(userJson)==false) { user = JsonSerializer.Deserialize<UserModel>(userJson); } if (user == null) //не смогли получить данные из кэша { user = _usersContext.Users.FirstOrDefault(x => x.Id == id); if (user != null) //данные в БД есть { _cache.SetString(id.ToString(), JsonSerializer.Serialize<UserModel>(user), new DistributedCacheEntryOptions() .SetAbsoluteExpiration(TimeSpan.FromSeconds(30)) //через 30 секунд элемент будет удален .SetSlidingExpiration(TimeSpan.FromSeconds(10)) //если в течение 10 секунд к объекту не будет обращения - он будет удален ); _logger.LogInformation($"Пользователь {user.Name} помещен в кэш"); } } else { _logger.LogInformation($"Пользователь {user.Name} был извлечен из кэша"); } return user; } } }
Класс UserModel
namespace AspRedisCache { public class UserModel { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } } }
Класс AppUsersContext
using Microsoft.EntityFrameworkCore; namespace AspRedisCache { 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" } ); } } }
Подобное приложение мы рассматривали, когда изучали работу с кэшированием в памяти, поэтому детально каждый момент работы приложения мы разбирать не будем. Стоит только отметить, что в приложении кэшируются данные о пользователях. Основная функциональность приложения по работе с распределенным кэшем сосредоточена в методе GetUser()
класса UserService
:
public UserModel GetUser(int id) { UserModel user = null; var userJson = _cache.GetString(id.ToString()); if (string.IsNullOrEmpty(userJson)==false) { user = JsonSerializer.Deserialize<UserModel>(userJson); } if (user == null) //не смогли получить данные из кэша { user = _usersContext.Users.FirstOrDefault(x => x.Id == id); if (user != null) //данные в БД есть { _cache.SetString(id.ToString(), JsonSerializer.Serialize<UserModel>(user), new DistributedCacheEntryOptions() .SetAbsoluteExpiration(TimeSpan.FromSeconds(30)) //через 30 секунд элемент будет удален .SetSlidingExpiration(TimeSpan.FromSeconds(10)) //если в течение 10 секунд к объекту не будет обращения - он будет удален ); _logger.LogInformation($"Пользователь {user.Name} помещен в кэш"); } } else { _logger.LogInformation($"Пользователь {user.Name} был извлечен из кэша"); } return user; }
Так как в распределенном кэше данные хранятся либо в виде строк, либо в виде массива байтов, то для сохранения элементов кэша был выбран формат Json, т.к. в C# можно довольно легко сериализировать/десериализировать объекты с использованием JSON.
Так, вначале мы проверяем наличие элемента в кэше:
var userJson = _cache.GetString(id.ToString()); if (string.IsNullOrEmpty(userJson)==false) { user = JsonSerializer.Deserialize<UserModel>(userJson); }
если элемент найден, то объект десериализуется и возвращается пользователю. Если же объект в кэше не найден, то он запрашивается из базы данных и записывается в кэш:
user = _usersContext.Users.FirstOrDefault(x => x.Id == id); if (user != null) //данные в БД есть { _cache.SetString(id.ToString(), JsonSerializer.Serialize<UserModel>(user), new DistributedCacheEntryOptions() .SetAbsoluteExpiration(TimeSpan.FromSeconds(30)) //через 30 секунд элемент будет удален .SetSlidingExpiration(TimeSpan.FromSeconds(10)) //если в течение 10 секунд к объекту не будет обращения - он будет удален ); _logger.LogInformation($"Пользователь {user.Name} помещен в кэш"); }
для этого мы сериализуем объект в JSON и передаем его в метод SetString()
сервиса кэширования. В третьем параметре мы задаем политики вытеснения, именно, указываем абсолютное и скользящее время вытеснения элемента. Опять же, эти значения полностью соответствуют аналогичным настройкам для кэша в памяти:
SetSlidingExpiration(TimeSpan.FromSeconds(10))
— говорит о том, что, если в течение 10 секунд к объекту кэша не было обращения, то он будет удаленSetAbsoluteExpiration(TimeSpan.FromSeconds(30))
— при этом, через 30 секунд элемент будет удален несмотря ни на что
В результате, в приложении мы увидим следующие записи лога:
Чтобы убедиться, что данные действительно сохраняются в Redis, запустим утилиту redis-cli.exe и выполним команду keys
с параметром *
:
Для получения информации об определенном элементе в кэше можно выполнить команду hgetall [ключ]
, например:
Итого
Использование распределенного кэша позволяет нам хранить кэш даже в том случае, если наше приложение завершает свою работу. При этом, кэш могут использовать несколько компьютеров одновременно. Для распределенного кэширования мы можем использовать различные сервисы, в том числе, как в рассмотренном примере — Redis. В целом, можно отметить, что использование распределенного кэширования в ASP.NET Core не многим сложнее, чем использование кэша в памяти, но, при этом, мы оперируем только двумя типами данных — строками и массивами байтов.