Skip to content

Nuxt Render Cache построен на принципах высокой производительности, распределенного кеширования и отказоустойчивости. Архитектура включает двухуровневую систему TTL, умные блокировки и интеграцию с Redis Pub/Sub для синхронизации между серверами.

Архитектура системы

Архитектурная диаграмма

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Nuxt Server   │    │  Redis Cache    │    │   Redis PubSub  │
│                 │    │                 │    │                 │
│  ┌──────────┐   │    │  ┌──────────┐   │    │  ┌──────────┐   │
│  │CacheRender│   │◄──►│  │   Data   │   │◄──►│  │ Messages │   │
│  │ Component │   │    │  │  Storage │   │    │  │  Queue   │   │
│  └──────────┘   │    │  └──────────┘   │    │  └──────────┘   │
│                 │    │                 │    │                 │
│  ┌──────────┐   │    │  ┌──────────┐   │    │  ┌──────────┐   │
│  │useRender │   │    │  │  Locks   │   │    │  │  Events  │   │
│  │  Cache   │   │◄──►│  │ Storage  │   │    │  │  System  │   │
│  └──────────┘   │    │  └──────────┘   │    │  └──────────┘   │
│                 │    │                 │    │                 │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                        │                        │
         └────────────────────────┼────────────────────────┘

                    ┌─────────────▼─────────────┐
                    │     REST API Layer       │
                    │                           │
                    │  ┌──────────┐ ┌──────────┐ │
                    │  │  Keys    │ │  Stats   │ │
                    │  │Management│ │  & Mon. │ │
                    │  └──────────┘ └──────────┘ │
                    └─────────────────────────────┘

Алгоритм кеширования

Принцип работы

Система кеширования Nuxt Render Cache следует определенному алгоритму принятия решений:

  1. Проверка кеша: Сначала проверяется наличие данных в Redis
  2. Оценка актуальности: Определяется статус TTL (актуален/протух/истек)
  3. Принятие решения: На основе статуса выбирается стратегия обработки
  4. Выполнение операции: Рендеринг, кеширование или возврат данных
  5. Синхронизация: Уведомление других процессов при необходимости

Поток принятия решений

Запрос → Проверка кеша → Оценка TTL
    ↓           ↓            ↓
Успех → Данные найдены → Soft TTL активен → Возврат из кеша
    ↓           ↓            ↓
Неудача → Данные не найдены → Soft TTL истек → Фоновое обновление
    ↓           ↓            ↓
    ↓           ↓            ↓
    ↓           ↓       Hard TTL истек → Полный рендеринг
    ↓           ↓            ↓
    ↓     Блокировка получена → Рендеринг и кеширование
    ↓           ↓            ↓
    ↓     Блокировка занята → Ожидание результата
    ↓           ↓            ↓
    ↓           ↓            ↓
Возврат результата ←───────┘

Двухуровневый TTL

Принцип работы

Система TTL состоит из двух уровней, каждый из которых служит определенной цели:

Время ──────────────────────────────────────────────────────────────▶
      │                    │                    │
      │    Fresh Zone      │   Stale Zone       │  Expired Zone
      │  (Soft TTL)        │ (Hard TTL)         │
      │                    │                    │
      ▼                    ▼                    ▼
   Soft TTL ───────────┬─ Hard TTL ──────────────▶ Expired
   (фоновое обновление)  (полное истечение)

Soft TTL (Фоновое обновление)

Назначение: Обеспечивает быструю отдачу контента пользователю во время фонового обновления.

Алгоритм:

  1. Пользователь запрашивает контент
  2. Система проверяет soft TTL
  3. Если soft TTL истек, но hard TTL еще актуален:
    • Немедленно возвращается старый контент
    • Запускается фоновое обновление кеша
    • Пользователь получает быстрый ответ
typescript
const handleSoftExpiredCache = async (
  slots: Slots,
  currentInstance: ComponentInternalInstance
): Promise<string> => {
  console.log(`[RenderCache] Кеш протух (softTTL) для ключа: ${cacheKey}`);

  // Получаем текущие данные из кеша
  const cachedEntry = await cache.get(cacheKey);
  if (!cachedEntry) {
    return await handleHardExpiredCache(slots, currentInstance);
  }

  // Пытаемся получить блокировку для фонового обновления
  const lockResult = await cache.lock(cacheKey, LOCK_TIMEOUT);

  if (lockResult.isLocked) {
    // Другой процесс уже обновляет
    console.log(
      `[RenderCache] Другой процесс обновляет кеш для ключа: ${cacheKey}`
    );
    return cachedEntry.data;
  }

  // Запускаем фоновое обновление
  setImmediate(async () => {
    try {
      await renderAndCache(slots, currentInstance);
      console.log(
        `[RenderCache] Фоновое обновление завершено для ключа: ${cacheKey}`
      );
    } catch (error) {
      console.error(`[RenderCache] Ошибка при фоновом обновлении:`, error);
    } finally {
      await lockResult.unlock();
    }
  });

  return cachedEntry.data;
};

Hard TTL (Полное истечение)

Назначение: Определяет максимальное время жизни кешированного контента.

Алгоритм:

  1. Пользователь запрашивает контент
  2. Система проверяет hard TTL
  3. Если hard TTL истек:
    • Контент считается полностью устаревшим
    • Выполняется полное перерендеринг
    • Используются блокировки для предотвращения одновременного рендеринга
typescript
const handleHardExpiredCache = async (
  slots: Slots,
  currentInstance: ComponentInternalInstance
): Promise<string> => {
  console.log(`[RenderCache] Кеш истек (hardTTL) для ключа: ${cacheKey}`);

  // Пытаемся получить блокировку
  const lockResult = await cache.lock(cacheKey, LOCK_TIMEOUT);

  if (lockResult.isLocked) {
    // Другой процесс уже рендерит
    console.log(`[RenderCache] Ожидаем завершения рендера другим процессом`);

    const backupCacheEntry = await cache.get(cacheKey);
    const newCacheEntry = await cache.waitForCache(cacheKey, backupCacheEntry);

    if (newCacheEntry) {
      return newCacheEntry.data;
    }

    throw new Error('Failed to get new cache entry');
  } else {
    // Мы получили блокировку, рендерим и кешируем
    try {
      return await renderAndCache(slots, currentInstance);
    } finally {
      await lockResult.unlock();
    }
  }
};

Система блокировок

Принцип работы

Система блокировок предотвращает одновременный рендеринг одного контента несколькими процессами:

Процесс A                Процесс B                Redis
    │                        │                     │
    │     Запрос контента    │                     │
    │───────────────────────▶│                     │
    │                        │                     │
    │      Проверка кеша     │                     │
    │      (hard TTL истек)  │                     │
    │                        │                     │
    │   Попытка получить     │                     │
    │   блокировку lock:key  │                     │
    │───────────────────────▶│                     │
    │                        │                     │
    │     Блокировка получена│                     │
    │◄───────────────────────│                     │
    │                        │                     │
    │    Начинаем рендеринг  │                     │
    │                        │                     │
    │     Запрос контента    │                     │
    │                        │◄────────────────────│
    │                        │                     │
    │                        │   Проверка кеша     │
    │                        │   (hard TTL истек)  │
    │                        │                     │
    │                        │Попытка получить     │
    │                        │блокировку lock:key  │
    │                        │────────────────────▶│
    │                        │                     │
    │                        │ Блокировка занята  │
    │                        │◄────────────────────│
    │                        │                     │
    │                        │   Ожидание через    │
    │                        │     Pub/Sub         │
    │                        │────────────────────▶│
    │                        │                     │
    │   Рендеринг завершен   │                     │
    │                        │                     │
    │   Сохраняем в кеш      │                     │
    │───────────────────────▶│                     │
    │                        │                     │
    │   Публикуем событие    │                     │
    │   cache:key обновлен   │                     │
    │───────────────────────▶│                     │
    │                        │                     │
    │                        │Получаем уведомление │
    │                        │◄────────────────────│
    │                        │                     │
    │                        │Получаем данные из   │
    │                        │кеша и возвращаем    │
    │                        │◄────────────────────│

Реализация блокировок

typescript
const lock = async (key: string, ttl: number) => {
  const lockKey = `lock:${key}`;
  const result = await redis.set(lockKey, 'locked', 'EX', ttl, 'NX');

  const isLocked = result !== 'OK';

  if (isLocked) {
    console.log(`[Cache] Лок ${lockKey} уже занят другим процессом`);
  } else {
    console.log(`[Cache] Лок ${lockKey} успешно получен, TTL: ${ttl}s`);
  }

  return {
    isLocked,
    unlock: async () => {
      console.log(`[Cache] Освобождаем лок ${lockKey}`);
      await redis.del(lockKey);
    },
  };
};

Ожидание через Pub/Sub

typescript
const waitForCache = async (
  key: string,
  backupEntry: CacheEntry | null,
  maxWaitTime: number = 5000
): Promise<CacheEntry | null> => {
  console.log(`[Cache] Начинаем ожидание данных для ключа: ${key}`);

  return new Promise<CacheEntry | null>((resolve, reject) => {
    let isResolved = false;
    const channel = `cache:${key}`;

    // Функция для завершения ожидания
    const cleanup = () => {
      if (!isResolved) {
        isResolved = true;
        redisSubscriber.unsubscribe(channel);
      }
    };

    // Устанавливаем таймаут
    const timeout = setTimeout(() => {
      console.log(`[Cache] Таймаут ожидания для ключа: ${key}`);
      cleanup();
      resolve(null);
    }, maxWaitTime);

    // Подписываемся на канал
    redisSubscriber.subscribe(channel, (err, count) => {
      if (err) {
        console.error(`[Cache] Ошибка подписки на канал ${channel}:`, err);
        cleanup();
        reject(err);
        return;
      }

      console.log(
        `[Cache] Подписались на канал ${channel}, активных подписок: ${count}`
      );
    });

    // Обработчик сообщений
    const messageHandler = (receivedChannel: string, message: string) => {
      if (receivedChannel === channel && !isResolved) {
        console.log(`[Cache] Получено событие для ключа: ${key}`);
        try {
          const cacheEntry = JSON.parse(message) as CacheEntry;
          resolve(cacheEntry);
        } catch (error) {
          console.error(
            `[Cache] Ошибка парсинга сообщения для ключа ${key}:`,
            error
          );
          reject(error);
        } finally {
          clearTimeout(timeout);
          cleanup();
        }
      }
    };

    redisSubscriber.on('message', messageHandler);
  });
};

Redis интеграция

Структура данных

typescript
// Запись кеша
type CacheEntry = {
  data: string; // HTML контент
  timestamp: number; // Время создания
  tags: string[]; // Теги для групповой инвалидации
};

// Ключ блокировки
type LockEntry = {
  key: `lock:${string}`; // lock:cache-key
  value: 'locked'; // Фиксированное значение
  ttl: number; // Время жизни блокировки
};

Схема ключей Redis

Redis Keys Structure:
├── cache:page:home          # Кешированный контент
├── cache:page:about         # Кешированный контент
├── cache:component:header   # Кешированный компонент
├── lock:page:home          # Блокировка рендеринга
├── lock:component:header   # Блокировка компонента
└── cache:api:products       # API данные

Pub/Sub каналы

Redis Pub/Sub Channels:
├── cache:page:home         # Уведомления об обновлении
├── cache:component:header  # Уведомления об обновлении
└── cache:api:products      # Уведомления об обновлении

Процесс рендеринга

Полный цикл рендеринга

mermaid
graph TD
    A[Пользователь запрашивает страницу] --> B[Проверка hard TTL]
    B --> C{Кеш истек?}
    C -->|Да| D[Попытка получить блокировку]
    C -->|Нет| E[Проверка soft TTL]
    D --> F{Блокировка получена?}
    F -->|Да| G[Рендеринг компонента]
    F -->|Нет| H[Ожидание через Pub/Sub]
    E --> I{Soft TTL истек?}
    I -->|Да| J[Фоновое обновление]
    I -->|Нет| K[Возврат из кеша]
    G --> L[Сохранение в Redis]
    L --> M[Публикация события]
    H --> N[Получение обновленных данных]
    J --> O[Фоновый рендеринг]
    O --> P[Сохранение в Redis]
    P --> Q[Публикация события]
    M --> R[Возврат пользователю]
    N --> R
    K --> R

Оптимизации производительности

1. Lazy Evaluation

Компоненты рендерятся только при первом запросе:

typescript
// Компонент рендерится только когда нужен
const renderCache = useRenderCache({
  cacheKey: 'expensive:component',
  hardTtl: 600,
  softTtl: 120,
});

// render будет вызван только при первом запросе
const html = await renderCache.render(slots, instance);

2. Memory Pooling

Эффективное использование памяти Redis:

typescript
// Автоматическая очистка устаревших данных
const cleanup = async () => {
  const keys = await redis.keys('cache:*');
  for (const key of keys) {
    const ttl = await redis.ttl(key);
    if (ttl === -1) {
      // Ключ без TTL
      await redis.del(key);
    }
  }
};

3. Connection Pooling

Переиспользование Redis соединений:

typescript
// Redis клиент переиспользуется между запросами
const redis = new Redis({
  host: process.env.REDIS_HOST,
  port: parseInt(process.env.REDIS_PORT || '6379'),
  maxRetriesPerRequest: 3,
  retryDelayOnFailover: 100,
  enableReadyCheck: false,
  // Connection pool settings
  maxRetriesPerRequest: null,
  lazyConnect: true,
});

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

Синхронизация между серверами

Сервер A                    Сервер B                    Redis
    │                          │                         │
    │   Запрос контента       │                         │
    │   (кеш отсутствует)     │                         │
    │                          │                         │
    │   Начинаем рендеринг    │                         │
    │                          │                         │
    │   Сохраняем в кеш       │                         │
    │─────────────────────────▶│                         │
    │                          │                         │
    │   Публикуем событие      │                         │
    │   cache:key:updated     │                         │
    │─────────────────────────▶│                         │
    │                          │                         │
    │                          │   Получаем событие      │
    │                          │◄────────────────────────│
    │                          │                         │
    │                          │  Обновляем локальный   │
    │                          │       кеш               │
    │                          │                         │
    │                          │   Следующий запрос      │
    │                          │   использует кеш       │
    │                          │                         │

Кластерная конфигурация

typescript
// Настройка Redis кластера
const redis = new Redis.Cluster(
  [
    {
      host: 'redis-node-1',
      port: 6379,
    },
    {
      host: 'redis-node-2',
      port: 6379,
    },
    {
      host: 'redis-node-3',
      port: 6379,
    },
  ],
  {
    redisOptions: {
      password: process.env.REDIS_PASSWORD,
    },
    clusterRetryDelay: 100,
    enableOfflineQueue: false,
  }
);

Обработка ошибок

Стратегия graceful degradation

typescript
const render = async (
  slots: Slots,
  currentInstance: ComponentInternalInstance
): Promise<string> => {
  try {
    // Основная логика кеширования
    const isHardExpired = await cache.expired(cacheKey, hardTtl);

    if (isHardExpired) {
      return await handleHardExpiredCache(slots, currentInstance);
    }

    const isSoftExpired = await cache.expired(cacheKey, softTtl);

    if (isSoftExpired) {
      return await handleSoftExpiredCache(slots, currentInstance);
    }

    // Кеш актуален
    const cachedEntry = await cache.get(cacheKey);
    if (cachedEntry) {
      return cachedEntry.data;
    }

    // Fallback
    return await renderAndCache(slots, currentInstance);
  } catch (error) {
    console.error(`[RenderCache] Ошибка при работе с кешем:`, error);

    // Graceful degradation - рендерим без кеша
    return await renderComponentToString(slots, currentInstance);
  }
};

Типы ошибок и их обработка

typescript
class CacheError extends Error {
  constructor(message: string, public code: string, public key?: string) {
    super(message);
    this.name = 'CacheError';
  }
}

// Типы ошибок
const CacheErrorCodes = {
  REDIS_CONNECTION_FAILED: 'REDIS_CONNECTION_FAILED',
  LOCK_ACQUISITION_FAILED: 'LOCK_ACQUISITION_FAILED',
  CACHE_CORRUPTION: 'CACHE_CORRUPTION',
  RENDER_TIMEOUT: 'RENDER_TIMEOUT',
  INVALID_KEY: 'INVALID_KEY',
} as const;

// Обработка специфических ошибок
const handleCacheError = (error: Error, key: string) => {
  if (error instanceof CacheError) {
    switch (error.code) {
      case CacheErrorCodes.REDIS_CONNECTION_FAILED:
        console.error(`Redis недоступен для ключа ${key}`);
        // Fallback логика
        break;

      case CacheErrorCodes.LOCK_ACQUISITION_FAILED:
        console.warn(`Не удалось получить блокировку для ${key}`);
        // Ожидание или fallback
        break;

      case CacheErrorCodes.RENDER_TIMEOUT:
        console.error(`Таймаут рендеринга для ${key}`);
        // Возврат старых данных или ошибка
        break;

      default:
        console.error(`Неизвестная ошибка кеша:`, error);
    }
  }
};

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

Ключевые метрики

typescript
interface CacheMetrics {
  // Производительность
  hitRate: number; // Процент попаданий в кеш
  missRate: number; // Процент промахов кеша
  averageResponseTime: number; // Среднее время ответа

  // Использование
  totalKeys: number; // Общее количество ключей
  memoryUsage: number; // Использование памяти
  connectedClients: number; // Подключенные клиенты

  // Операции
  operationsPerSecond: number; // Операций в секунду
  evictionRate: number; // Скорость вытеснения
  lockContention: number; // Конфликты блокировок
}

Сбор метрик

typescript
const collectMetrics = async (): Promise<CacheMetrics> => {
  const info = await redis.info();
  const stats = await redis.getStats();

  // Парсинг метрик из Redis INFO
  const metrics: CacheMetrics = {
    hitRate: calculateHitRate(info),
    missRate: calculateMissRate(info),
    averageResponseTime: calculateAverageResponseTime(stats),
    totalKeys: parseInt(info['db0:keys'] || '0'),
    memoryUsage: parseInt(info['used_memory'] || '0'),
    connectedClients: parseInt(info['connected_clients'] || '0'),
    operationsPerSecond: parseFloat(info['instantaneous_ops_per_sec'] || '0'),
    evictionRate: parseInt(info['evicted_keys'] || '0'),
    lockContention: await calculateLockContention(),
  };

  return metrics;
};

Масштабируемость

Горизонтальное масштабирование

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   Load      │    │   Redis     │    │   Redis     │
│  Balancer   │    │  Cluster    │    │  Cluster    │
│             │    │   Node 1    │    │   Node 2    │
└──────┬──────┘    └──────┬──────┘    └──────┬──────┘
       │                  │                  │
       └─────────┬────────┼─────────┬────────┘
                 │        │         │
        ┌────────▼────────┴─────────▼────────┐
        │                                   │
        │         Application Servers        │
        │                                   │
        │  ┌─────────┐ ┌─────────┐ ┌─────────┐ │
        │  │ Server  │ │ Server  │ │ Server  │ │
        │  │   1     │ │   2     │ │   3     │ │
        │  └─────────┘ └─────────┘ └─────────┘ │
        └─────────────────────────────────────┘

Оптимизации для высокой нагрузки

  1. Redis Cluster: Распределение данных между несколькими узлами
  2. Read Replicas: Реплики для операций чтения
  3. Connection Pooling: Переиспользование соединений
  4. Batch Operations: Групповые операции для снижения overhead
  5. Memory Management: Управление памятью и политиками вытеснения
typescript
// Оптимизированная конфигурация для высокой нагрузки
const redis = new Redis.Cluster(clusterNodes, {
  redisOptions: {
    // Оптимизации производительности
    maxRetriesPerRequest: 3,
    retryDelayOnFailover: 100,
    enableReadyCheck: false,

    // Connection pooling
    maxRetriesPerRequest: null,
    lazyConnect: true,

    // Memory management
    maxmemory: '2gb',
    maxmemoryPolicy: 'allkeys-lru',
  },

  // Cluster settings
  clusterRetryDelay: 100,
  enableOfflineQueue: false,
  scaleReads: 'slave', // Чтение из реплик
});

Безопасность

Защита Redis

typescript
// Безопасная конфигурация Redis
const redis = new Redis({
  host: process.env.REDIS_HOST,
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD,

  // TLS/SSL
  tls: {
    cert: process.env.REDIS_CERT,
    key: process.env.REDIS_KEY,
    ca: process.env.REDIS_CA,
  },

  // ACL (Redis 6.0+)
  username: process.env.REDIS_USERNAME,
  password: process.env.REDIS_PASSWORD,
});

Валидация данных

typescript
// Валидация ключей кеша
const validateCacheKey = (key: string): boolean => {
  // Проверяем длину
  if (key.length === 0 || key.length > 250) {
    return false;
  }

  // Проверяем символы (только безопасные)
  if (!/^[a-zA-Z0-9:_-]+$/.test(key)) {
    return false;
  }

  // Проверяем запрещенные паттерны
  const forbiddenPatterns = [
    /^\.\./, // Directory traversal
    /^\/etc/, // System files
    /<script/i, // XSS attempts
  ];

  return !forbiddenPatterns.some((pattern) => pattern.test(key));
};

// Валидация тегов
const validateTags = (tags: string[]): boolean => {
  return tags.every(
    (tag) => tag.length > 0 && tag.length <= 50 && /^[a-zA-Z0-9_-]+$/.test(tag)
  );
};

Производительность

Бенчмарки

Тестовый сценарий: 1000 одновременных запросов

Без кеширования:
- Среднее время ответа: 180ms
- CPU использование: 85%
- Память: 150MB
- RPS: ~50

С Nuxt Render Cache:
- Среднее время ответа: 3ms (60x быстрее)
- CPU использование: 15% (5.6x экономия)
- Память: 45MB (3x экономия)
- RPS: ~5000
- Cache hit rate: 95%

Оптимизации

  1. Serialization: Эффективная сериализация данных
  2. Compression: Сжатие больших HTML фрагментов
  3. Lazy Loading: Отложенная загрузка компонентов
  4. Memory Limits: Ограничения памяти Redis
  5. TTL Optimization: Автоматическая настройка TTL на основе паттернов использования
typescript
// Оптимизированная сериализация
const serializeCacheEntry = (entry: CacheEntry): string => {
  // Сжатие для больших HTML фрагментов
  if (entry.data.length > 1024) {
    return JSON.stringify({
      ...entry,
      data: compress(entry.data), // LZ4 или Gzip
      compressed: true,
    });
  }

  return JSON.stringify(entry);
};

const deserializeCacheEntry = (data: string): CacheEntry => {
  const parsed = JSON.parse(data);

  if (parsed.compressed) {
    return {
      ...parsed,
      data: decompress(parsed.data),
    };
  }

  return parsed;
};

Заключение

Архитектура Nuxt Render Cache обеспечивает:

  • Высокую производительность через двухуровневую систему TTL
  • Отказоустойчивость благодаря умным блокировкам и Pub/Sub
  • Масштабируемость с Redis кластерами и распределенным кешированием
  • Надежность через graceful degradation и обработку ошибок
  • Мониторимость с подробными метриками и логированием

Эта архитектура позволяет эффективно кешировать рендеринг Vue компонентов в высоконагруженных приложениях с минимальным влиянием на пользовательский опыт.

Создано с ❤️ для Nuxt сообщества