Что такое двухуровневый кэш (L1/L2) в архитектуре приложений?

«Что такое двухуровневый кэш (L1/L2) в архитектуре приложений?» — вопрос из категории Архитектура, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Двухуровневый кэш — это паттерн, при котором система кэширования состоит из двух уровней: быстрого, но малого по объёму кэша первого уровня (L1) и более медленного, но ёмкого кэша второго уровня (L2). Цель — минимизировать задержки при доступе к данным и снизить нагрузку на основное хранилище (например, БД).

Типичная реализация в веб-приложении:

  • L1 (In-Memory Cache): Быстрый кэш в памяти процесса приложения (например, IMemoryCache в .NET).
  • L2 (Distributed Cache): Распределённый кэш, доступный нескольким экземплярам приложения (например, Redis).

Пример кода на C#:

public class TwoLevelCacheService
{
    private readonly IMemoryCache _memoryCache; // L1
    private readonly IDistributedCache _distributedCache; // L2
    private readonly IDataSource _primarySource; // БД или внешний API

    public async Task<string> GetDataAsync(string key)
    {
        // 1. Проверяем L1
        if (_memoryCache.TryGetValue(key, out string cachedData))
        {
            return cachedData;
        }

        // 2. Проверяем L2
        var data = await _distributedCache.GetStringAsync(key);
        if (data != null)
        {
            // Обновляем L1 для будущих быстрых запросов
            _memoryCache.Set(key, data, TimeSpan.FromMinutes(1));
            return data;
        }

        // 3. Обращаемся к первичному источнику (промах в кэшах)
        data = await _primarySource.FetchDataAsync(key);

        // 4. Заполняем оба уровня кэша
        await _distributedCache.SetStringAsync(key, data, new DistributedCacheEntryOptions {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
        });
        _memoryCache.Set(key, data, TimeSpan.FromMinutes(5));

        return data;
    }
}

Преимущества и недостатки:

Преимущество Недостаток
Снижение задержки: Частые запросы обслуживаются из быстрого L1. Сложность согласованности: Необходима стратегия инвалидации данных в обоих кэшах.
Уменьшение нагрузки на L2 и БД: L1 поглощает часть трафика. Дополнительные издержки: Усложнение кода и инфраструктуры.
Масштабируемость: L2 (распределённый) обеспечивает консистентность между нодами. Риск устаревших данных: При неправильной TTL данные в L1 могут быть stale.