Кэширование в C#: как ускорить приложение и сэкономить ресурсы

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

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

Начать курс

Почему кэширование — это суперсила каждого C# разработчика

Представьте: ваше приложение каждый раз ходит в базу данных за одним и тем же списком городов или курсом валют. База данных — это медленный и дорогой ресурс. А что если мы скажем, что можно ответить пользователю за 1 миллисекунду вместо 100? Да, это не магия, а грамотное кэширование данных в C#.

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

Виды кэширования в C#: от MemoryCache до Redis

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

1. Встроенный MemoryCache (System.Runtime.Caching / Microsoft.Extensions.Caching.Memory)

Самый простой и быстрый способ — использовать IMemoryCache. Данные хранятся в оперативной памяти процесса. Идеально для однопоточных приложений или простых веб-сайтов.

Пример регистрации и использования в ASP.NET Core:

// В Program.cs
builder.Services.AddMemoryCache();

// В контроллере или сервисе
public class WeatherService
{
    private readonly IMemoryCache _cache;

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

    public async Task<string> GetWeatherAsync(string city)
    {
        string cacheKey = $"weather_{city}";

        // Пытаемся получить данные из кэша
        if (_cache.TryGetValue(cacheKey, out string cachedData))
        {
            return cachedData;
        }

        // Если данных нет — загружаем (например, из API)
        string weatherData = await FetchWeatherFromApi(city);

        // Сохраняем в кэш на 10 минут
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(10));

        _cache.Set(cacheKey, weatherData, cacheEntryOptions);

        return weatherData;
    }
}

Ключевые методы: TryGetValue, Set, Remove. Обратите внимание на политики истечения: абсолютное время (данные умрут ровно через N минут) и скользящее время (данные живут, пока к ним обращаются, и N минут после последнего запроса).

2. Распределённый кэш: Redis через IDistributedCache

Если у вас несколько экземпляров приложения (веб-ферма, микросервисы), MemoryCache не подходит — каждый сервер будет хранить свою копию. На помощь приходит Redis. Он работает как отдельный сервер, общий для всех инстансов.

Подключение Redis в C#:

// Установите пакет: Microsoft.Extensions.Caching.StackExchangeRedis
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "SampleInstance";
});

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

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

    public async Task<Product> GetProductAsync(int id)
    {
        string cacheKey = $"product_{id}";
        var cachedProduct = await _cache.GetStringAsync(cacheKey);

        if (cachedProduct != null)
        {
            return JsonSerializer.Deserialize<Product>(cachedProduct);
        }

        var product = await FetchProductFromDb(id);
        var serialized = JsonSerializer.Serialize(product);

        await _cache.SetStringAsync(cacheKey, serialized, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
        });

        return product;
    }
}

Кэширование данных в C# через Redis даёт консистентность и отказоустойчивость. Но помните: сериализация/десериализация добавляет небольшую задержку.

3. Гибридное кэширование (L1 + L2)

Современный тренд — комбинировать in-memory кэш (L1) и Redis (L2). Данные сначала ищутся в памяти (быстро), а при промахе — в Redis. Это снижает нагрузку на сеть и Redis.

Пример с библиотекой FusionCache (рекомендую):

// Установите: ZiggyCreatures.FusionCache
builder.Services.AddFusionCache()
    .WithDefaultEntryOptions(new FusionCacheEntryOptions
    {
        Duration = TimeSpan.FromMinutes(5)
    })
    .WithDistributedCache(
        new RedisCache(new RedisCacheOptions { Configuration = "localhost:6379" })
    );

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

    public UserService(IFusionCache cache)
    {
        _cache = cache;
    }

    public async Task<User> GetUserAsync(int id)
    {
        return await _cache.GetOrSetAsync<User>(
            $"user_{id}",
            async _ => await GetUserFromDb(id),
            TimeSpan.FromMinutes(10)
        );
    }
}

FusionCache сам управляет синхронизацией L1 и L2, поддерживает "защиту от лавины запросов" (cache stampede) и мягкое истечение (fail-safe). Это мощный инструмент для кэширования в .NET.

Стратегии истечения кэша: когда данные становятся "свежими"

Недостаточно просто сохранить данные — нужно решить, когда их удалять. Вот основные стратегии:

  • Absolute expiration — данные живут ровно N минут. Подходит для курсов валют, расписаний.
  • Sliding expiration — данные удаляются, если к ним не обращались N минут. Отлично для сессий пользователей.
  • Упреждающее удаление (Cache invalidation) — вы сами удаляете кэш, когда данные изменяются (например, после обновления товара в админке).

Пример ручной инвалидации:

public async Task UpdateProduct(Product product)
{
    // Обновляем БД
    await _db.Products.UpdateAsync(product);
    
    // Удаляем кэш
    string cacheKey = $"product_{product.Id}";
    _cache.Remove(cacheKey);
    
    // Или обновляем кэш сразу (cache-aside)
    var serialized = JsonSerializer.Serialize(product);
    await _cache.SetStringAsync(cacheKey, serialized, ...);
}

Продвинутые техники: кэширование в облаке и паттерны

Кэширование в Azure и AWS

В облачных средах используйте managed-сервисы: Azure Redis Cache или Amazon ElastiCache. Подключение через StackExchange.Redis идентично локальному Redis, только строка подключения будет содержать ключ доступа.

Паттерн "Cache-Aside" (Ленивое кэширование)

Это то, что мы делали в примерах выше: приложение сначала проверяет кэш, если нет — загружает данные из источника и сохраняет в кэш. Это самый распространённый паттерн для <

Блоги

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