Продвинутое кэширование в C#: Redis, Memcached и встроенные решения

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему продвинутое кэширование меняет всё

В современной высоконагруженной архитектуре скорость ответа и снижение нагрузки на базу данных — ключевые задачи. Кэширование в C# (.NET) перестало быть просто запоминанием результата: это сложная система управления состоянием, временем жизни и согласованностью данных. На продвинутом уровне разработчик должен выбирать между Redis, Memcached и встроенными решениями в зависимости от сценария.

В этой статье мы разберём три основных подхода к кэшированию в экосистеме .NET, сравним их производительность, сложность внедрения и типичные ошибки. Вы узнаете, как настроить распределённый кэш с Redis, оптимизировать Memcached для высоких нагрузок и когда встроенный IMemoryCache — лучшее решение.

Встроенное кэширование в .NET: IMemoryCache и IDistributedCache

IMemoryCache — быстрый локальный кэш

Встроенный кэш IMemoryCache из пространства имён Microsoft.Extensions.Caching.Memory — идеальное решение для одного экземпляра приложения. Он хранит данные в оперативной памяти процесса, обеспечивая минимальную задержку.

// Регистрация в Startup.cs или Program.cs
builder.Services.AddMemoryCache();

// Использование в сервисе
public class ProductService
{
    private readonly IMemoryCache _cache;

    public ProductService(IMemoryCache cache)
    {
        _cache = cache;
    }

    public async Task<Product> GetProductAsync(int id)
    {
        string cacheKey = $"product_{id}";
        if (!_cache.TryGetValue(cacheKey, out Product product))
        {
            product = await _repository.GetProductByIdAsync(id);
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .SetAbsoluteExpiration(TimeSpan.FromMinutes(5))
                .SetSlidingExpiration(TimeSpan.FromMinutes(1))
                .SetPriority(CacheItemPriority.High);

            _cache.Set(cacheKey, product, cacheEntryOptions);
        }
        return product;
    }
}

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

Минусы: не работает при масштабировании на несколько серверов; ограничен объёмом RAM процесса.

IDistributedCache — абстракция для распределённых кэшей

Интерфейс IDistributedCache позволяет легко переключаться между Redis, SQL Server или NCache без изменения бизнес-логики. Реализация по умолчанию — MemoryDistributedCache (локальный), но для продакшена обычно выбирают Redis.

// Регистрация Redis-реализации
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "SampleInstance";
});

// Использование
public async Task SetCacheAsync(string key, byte[] data)
{
    var cache = context.RequestServices.GetRequiredService<IDistributedCache>();
    await cache.SetAsync(key, data, new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
    });
}

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

Redis в C#: продвинутые техники и паттерны

Подключение и пул соединений

Для работы с Redis в .NET используется библиотека StackExchange.Redis. Вместо создания нового соединения каждый раз, используйте мультиплексирование — одно соединение на всё приложение.

// Глобальный синглтон
public static ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost:6379,abortConnect=false");

// Получение базы данных
IDatabase db = redis.GetDatabase(0);

// Базовые операции
await db.StringSetAsync("user:1", "John Doe", TimeSpan.FromMinutes(30));
string name = await db.StringGetAsync("user:1");

Продвинутый совет: используйте каналы (pub/sub) для инвалидации кэша на всех узлах кластера:

// Подписка на канал инвалидации
var sub = redis.GetSubscriber();
sub.Subscribe("cache-invalidation", (channel, message) =>
{
    string keyToInvalidate = message;
    db.KeyDelete(keyToInvalidate);
    Console.WriteLine($"Кэш {keyToInvalidate} сброшен");
});

// Публикация события инвалидации
await sub.PublishAsync("cache-invalidation", "product_123");

Стратегии истечения и вытеснения

Redis поддерживает несколько политик вытеснения: allkeys-lru, volatile-ttl, noeviction. Для продвинутых сценариев комбинируйте:

  • Абсолютное истечение — для данных, которые устаревают в определённый момент (например, токены).
  • Скользящее истечение — для сессий пользователей (обновляется при каждом запросе).
  • Ленивая инвалидация — проверка версии данных через дополнительный ключ-счётчик.
// Пример ленивой инвалидации с версионированием
string versionKey = "product:version:123";
string dataKey = "product:data:123";

long currentVersion = await db.StringIncrementAsync(versionKey); // +1 при обновлении
await db.StringSetAsync(dataKey, serializedData, TimeSpan.FromMinutes(10));

// При чтении
long storedVersion = (long)await db.StringGetAsync(versionKey);
if (storedVersion != expectedVersion) { /* перезагружаем */ }

Memcached в .NET: когда скорость важнее структуры

Особенности и отличия от Redis

Memcached — это простейший распределённый кэш «ключ-значение» без персистентности и сложных типов данных. В .NET используется клиент EnyimMemcached или Memcached.ClientLibrary. Его главное преимущество — сверхнизкая задержка (до 1 мс) и линейная масштабируемость.

// Настройка клиента
var config = new MemcachedClientConfiguration();
config.AddServer("localhost", 11211);
var client = new MemcachedClient(config);

// Сохранение и чтение
await client.StoreAsync(StoreMode.Set, "user:42", userObject, TimeSpan.FromMinutes(10));
var cachedUser = await client.GetAsync<User>("user:42");

Когда выбирать Memcached вместо Redis:

  • Вам нужен только простой кэш без

Блоги

Рекомендаций по книгам