Распределенное кэширование. Redis

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

Распределенное кэширование

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

В .NET распределенный кэш представлен интерфейсом IDistributedCache. Для управления кэшем этот тип предоставляет ряд методов. Основные из них:

  • Get(string key) / GetAsync(string key, CancellationToken token): возвращает значение с указанным ключом. значение возвращается в виде массива байтов byte[]?

  • Refresh(string key) / RefreshAsync(string key, CancellationToken token): обновляет значение в кэше по ключу, сбрасывая его срок кэширования.

  • Remove(string key) / RemoveAsync(string key, CancellationToken token): удаляет значение с указанным ключом.

  • Set(string key, byte[] value, DistributedCacheEntryOptions options) / SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token): устанавливает значение с указанным ключом. В качестве значения выступает массив байтов

  • SetString(string key, string value) / SetStringAsync(string key, string value, CancellationToken token): устанавливает значение с указанным ключом. В качестве значения выступает строка

  • GetString(string key) / GetStringAsync(string key, CancellationToken token): возвращает значение с указанным ключом. Значение возвращается в виде строки

Из перечисления методов видно, что мы можем сохранить в кэш данные либо в виде массива байт byte[], либо в виде строки string. И обратно получить данные из кэша также можно только в виде объектов byte[] и string. Соответственно если наши данные представляют другой тип, то нам надо предусмотреть механиз сериализации/десериализации данные в/из byte[] и string.

Есть разные реализации распределенного кэша. Рассмотрим одну из наиболее популярных, которую представляет Redis.

Установка Redis

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

Прежде всего для работы нам нужен сам сервер Redis. В рамках данной статьи будем использовать локальный сервер Redis, который установден на текущем компьютере. Однако на данный момент Redis работает только на Linux и на MacOS.

MacOS

На MacOS для установки Redis применяется пакетный менеджер homebrew :

brew install redis

Для запуска на MacOS применяется команда

sudo service redis-server start

Linux

На Linux для установки Redis надо выполнить следующие команды:

curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg

echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list

sudo apt-get update
sudo apt-get install redis

Для запуска на Linux применяется команда

sudo service redis-server start

Windows

Для Windows Redis не доступен, однако можно использовать Windows Subsystem for Linux или WSL. Для этого сначала надо установить wsl командой

wsl --install

Затем откроем консоль WSL и выполним все те же команды, что указаны выше для Linux.

Для проверки установки Redis можно посмотреть версию командой redis-server --version

Установка Redis для приложения ASP.NET Core на C# на WSL

Подключение к Redis

Фреймворк ASP.NET Core предоставляет встроенные инструменты для работы с Redis. Так, создадим проект ASP.NET Core. Для работы с Redis добавим в проект Nuget-пакет Microsoft.Extensions.Caching.StackExchangeRedis

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

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

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

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;

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.AddStackExchangeRedisCache(options => {
    options.Configuration = "localhost";
    options.InstanceName = "local";
});
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;
    IDistributedCache cache;
    public UserService(ApplicationContext context, IDistributedCache distributedCache)
    {
        db = context;
        cache = distributedCache;
    }
    public async Task<User?> GetUser(int id)
    {
        User? user = null;
        // пытаемся получить данные из кэша по id
        var userString = await cache.GetStringAsync(id.ToString());
        //десериализируем из строки в объект User
        if (userString != null) user = JsonSerializer.Deserialize<User>(userString);

        // если данные не найдены в кэше
        if (user == null)
        {
            // обращаемся к базе данных
            user = await db.Users.FindAsync(id);
            // если пользователь найден, то добавляем в кэш
            if (user != null)
            {
                Console.WriteLine($"{user.Name} извлечен из базы данных");
                // сериализуем данные в строку в формате json
                userString = JsonSerializer.Serialize(user);
                // сохраняем строковое представление объекта в формате json в кэш на 2 минуты
                await cache.SetStringAsync(user.Id.ToString(), userString, new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
                });
            }
        }
        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;
    IDistributedCache cache;
    public UserService(ApplicationContext context, IDistributedCache distributedCache)
    {
        db = context;
        cache = distributedCache;
    }
    public async Task<User?> GetUser(int id)
    {
        User? user = null;
        // пытаемся получить данные из кэша по id
        var userString = await cache.GetStringAsync(id.ToString());
        //десериализируем из строки в объект User
        if (userString != null) user = JsonSerializer.Deserialize<User>(userString);

        // если данные не найдены в кэше
        if (user == null)
        {
            // обращаемся к базе данных
            user = await db.Users.FindAsync(id);
            // если пользователь найден, то добавляем в кэш
            if (user != null)
            {
                Console.WriteLine($"{user.Name} извлечен из базы данных");
                // сериализуем данные в строку в формате json
                userString = JsonSerializer.Serialize(user);
                // сохраняем строковое представление объекта в формате json в кэш на 2 минуты
                await cache.SetStringAsync(user.Id.ToString(), userString, new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
                });
            }
        }
        else
        {
            Console.WriteLine($"{user.Name} извлечен из кэша");
        }
        return user;
    }
}

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

Также через механизм внедрения зависимостей сервис в конструкторе получает сервис IDistributedCache, через который приложение будет взаимодействовать с сервером Redis.

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

var userString = await cache.GetStringAsync(id.ToString());

Здесь ключами элементов в кэше являются значения id. Если ключ в кэше был найден, то десериализуем полученное значение с помощью метода JsonSerializer.Deserialize()

if (userString != null) user = JsonSerializer.Deserialize<User>(userString);

Если в кэше не оказалось объекта, то извлекаем его и бд, сериализуем его в строку и затем добавляем в кэш на 2 минуты:

user = await db.Users.FindAsync(id);
if (user != null)
{
    Console.WriteLine($"{user.Name} извлечен из базы данных");
    userString = JsonSerializer.Serialize(user);
    await cache.SetStringAsync(user.Id.ToString(), userString, new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
    });
}

Для добавления в кэш в метод SetStringAsync() передаем ключ объекта - его Id, затем передаем само кэшируемое значение - сериализованный объект User. И далее устанавливаем опции кэширования с помощью объекта DistributedCacheEntryOptions. В частности, с помощью свойства AbsoluteExpirationRelativeToNow задаем время кэширования относительно текущего момента в виде 2 минут.

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

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

builder.Services.AddStackExchangeRedisCache(options => {
    options.Configuration = "localhost";
    options.InstanceName = "local";
});

Метод AddStackExchangeRedisCache() в качестве параметра принимает делегат Action<Microsoft.Extensions.Caching.StackExchangeRedis.RedisCacheOptions>. Его параметр - объект RedisCacheOptions позволяет установить настройки подключения с помощью своих свойств:

  • Configuration: конфигурация подключения к серверу Redis. Для подключения к локально запущенному серверу можно указать, как в примере выше, строку "localhost". Для подключения к внешним серверам Redis на других хостах указываются адрес хоста и порт, соответственно точную конфигурацию надо узнавать у хостера.

  • InstanceName: имя экземпляра Redis. Для локального экземпляра можно указать "local". Это имя будет использоваться в качестве префикса для сохраняемых в кэше данных.

И для тестирования определим конечную точку, где клиент передает 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";
});

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

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

А в кэше Redis мы можем увидеть сохраненный объект. Для этого нам надо в консоли обратиться к redis-cli с помощью команды

redis-cli

Затем получить все ключи с помощью команды

keys *

Поскольку в настройках конфигурации подключения Redis в коде C# в качестве имени экземпляра задана строка "local", а ключ сохраняенного объекта - это его id, то есть число id, то в самом Redis сохраненный объект будет храниться по ключу "local1". И для получения данных по записи с этим ключом выполним команду

hgetall local1

То есть команде hgetall передается ключ записи

Получение из сервера Redis сохраненной записи по ключу в приложении на ASP.NET Core и C#

DistributedCacheEntryOptions

Для установки параметров кэширования в метод Set/SetAsync/SetString/SetStringAsync в качестве третьего параметра можно передать объект DistributedCacheEntryOptions, который устанавливает настройки кэширования объекта с помощью ряда свойств:

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

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

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

  • Это значение не увеличивает время кэширования записи - оно по прежнему остается в рамках абсолютной даты кэширования (если она задана).

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