← Назад к вопросам

Что использовал для кэша?

1.2 Junior🔥 161 комментариев
#Кэширование и Redis

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Мой опыт работы с кэшированием в C# Backend

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

Распределённые кэши для масштабируемых систем

Redis стал моим основным инструментом для большинства production-проектов:

// Пример использования StackExchange.Redis
public class RedisCacheService : ICacheService
{
    private readonly IDatabase _redisDb;
    
    public async Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan expiry)
    {
        var cached = await _redisDb.StringGetAsync(key);
        if (!cached.IsNull)
            return JsonConvert.DeserializeObject<T>(cached);
        
        var data = await factory();
        await _redisDb.StringSetAsync(key, 
            JsonConvert.SerializeObject(data), 
            expiry);
        return data;
    }
}

Преимущества Redis в моей практике:

  • Высокая производительность - операции за микросекунды
  • Поддержка сложных структур данных (хеши, списки, множества)
  • Публикация/подписка для инвалидации кэша в кластере
  • Автоматическое удаление с TTL

In-memory кэши для приложений с одной инстанс

IMemoryCache из ASP.NET Core идеален для сценариев:

  • Single-instance приложения
  • Быстрых локальных кэшей "второго уровня"
  • Кэширования данных, специфичных для текущего пользователя или сессии
// Комбинированный подход: двухуровневый кэш
public class HybridCacheService : ICacheService
{
    private readonly IMemoryCache _memoryCache;
    private readonly IDistributedCache _distributedCache;
    
    public async Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> factory)
    {
        // Сначала проверяем локальный кэш
        if (_memoryCache.TryGetValue(key, out T memoryCached))
            return memoryCached;
        
        // Затем распределённый
        var distributedCached = await _distributedCache.GetStringAsync(key);
        if (distributedCached != null)
        {
            var result = JsonConvert.DeserializeObject<T>(distributedCached);
            // Записываем в локальный кэш с меньшим TTL
            _memoryCache.Set(key, result, TimeSpan.FromSeconds(30));
            return result;
        }
        
        // Если нет нигде - выполняем фабрику
        var data = await factory();
        await CacheOnAllLevels(key, data);
        return data;
    }
}

Оптимизации и паттерны, которые я применяю

  1. Cache-aside (Lazy Loading)

    • Самый частый паттерн в моей практике
    • Данные загружаются по требованию
    • Полный контроль над инвалидацией
  2. Cache Stampede Protection

    • Использование SemaphoreSlim для предотвращения "толпы кэша"
    • Блокировка на время выполнения тяжёлых запросов
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new();

public async Task<T> GetOrSetWithStampedeProtection<T>(string key, Func<Task<T>> factory)
{
    if (_memoryCache.TryGetValue(key, out T cachedValue))
        return cachedValue;
    
    var lockObj = _locks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
    
    await lockObj.WaitAsync();
    try
    {
        // Двойная проверка после захвата блокировки
        if (_memoryCache.TryGetValue(key, out cachedValue))
            return cachedValue;
        
        cachedValue = await factory();
        _memoryCache.Set(key, cachedValue, TimeSpan.FromMinutes(5));
    }
    finally
    {
        lockObj.Release();
        _locks.TryRemove(key, out _);
    }
    
    return cachedValue;
}
  1. Топологии инвалидации кэша:
    • TTL-based - для данных, которые можно перезагрузить
    • Event-driven - подписка на изменения в БД через CDC
    • Write-through - для критически важных данных

Мониторинг и метрики

В production всегда настраиваю:

  • Hit/Miss Ratio для каждого типа кэша
  • Среднее время выполнения операций с кэшем
  • Использование памяти Redis кластера
  • Количество подключений и сетевую задержку

Выбор конкретного решения зависит от:

  • Частоты чтения vs записи - для read-heavy лучше Redis, для write-heavy нужна осторожность
  • Требований к консистентности - strong vs eventual consistency
  • Бюджета и инфраструктуры - Redis кластер vs управляемый сервис в облаке
  • Объёма данных - in-memory для ГБ, Redis для ТБ данных

В последних проектах всё чаще использую Redis с persistence, разделение кэшей по назначению (сессионный, data-cache, output-cache), и автоматическое переключение на circuit breaker при проблемах с кэш-сервером. Ключевой принцип: кэш должен быть transparent layer, а не источником проблем в распределённой системе.