В этом руководстве мы создадим масштабируемый сервис сокращения URL с использованием Node.js и Redis. Этот сервис будет использовать распределенное кэширование для эффективной обработки высокой нагрузки, снижения задержек и плавного масштабирования. Мы рассмотрим ключевые концепции, такие как согласованное хеширование, стратегии инвалидации кэша и шардинг, чтобы обеспечить быструю и надежную работу системы.

К концу этого руководства у вас будет полностью функциональный сервис сокращения URL, который использует распределенное кэширование для оптимизации производительности. Мы также создадим интерактивное демо, где пользователи смогут вводить URL-адреса и видеть метрики в реальном времени, такие как попадания и промахи кэша.

Что вы узнаете

  • Как создать сервис сокращения URL с использованием Node.js и Redis.

  • Как реализовать распределенное кэширование для оптимизации производительности.

  • Понимание согласованного хеширования и стратегий инвалидации кэша.

  • Использование Docker для моделирования нескольких экземпляров Redis для шардинга и масштабирования.

Предварительные требования

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

  • Node.js (v14 или более поздняя версия)

  • Redis

  • Docker

  • Базовые знания JavaScript, Node.js и Redis.

Содержание

Обзор проекта

Мы создадим сервис для сокращения URL-адресов, где:

  1. Пользователи могут сокращать длинные URL-адреса и получать оригинальные URL-адреса.

  2. Сервис использует кэширование Redis для хранения соответствий между сокращенными URL-адресами и оригинальными URL-адресами.

  3. Кэш распределен по нескольким экземплярам Redis для обработки высокой нагрузки.

  4. Система будет демонстрировать попадания в кэш и промахи в реальном времени.

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

Для обеспечения масштабируемости и производительности мы разделим наш сервис на следующие компоненты:

  1. Сервер API: Обрабатывает запросы на сокращение и извлечение URL-адресов.

  2. Слой кеширования Redis: Использует несколько экземпляров Redis для распределенного кеширования.

  3. Docker: Моделирует распределенную среду с несколькими контейнерами Redis.

Шаг 1: Настройка проекта

Давайте настроим наш проект, инициализируя приложение Node.js:

mkdir scalable-url-shortener
cd scalable-url-shortener
npm init -y

Теперь установите необходимые зависимости:

npm install express redis shortid dotenv
  • express: Легкий веб-серверный фреймворк.

  • redis: Для работы с кешированием.

  • shortid: Для генерации коротких уникальных идентификаторов.

  • dotenv: Для управления переменными среды.

Создайте файл .env в корне вашего проекта:

PORT=3000
REDIS_HOST_1=localhost
REDIS_PORT_1=6379
REDIS_HOST_2=localhost
REDIS_PORT_2=6380
REDIS_HOST_3=localhost
REDIS_PORT_3=6381

Эти переменные определяют хосты и порты Redis, которые мы будем использовать.

Шаг 2: Настройка экземпляров Redis

Мы будем использовать Docker для имитации распределенной среды с несколькими экземплярами Redis.

Запустите следующие команды для запуска трех контейнеров Redis:

docker run -p 6379:6379 --name redis1 -d redis
docker run -p 6380:6379 --name redis2 -d redis
docker run -p 6381:6379 --name redis3 -d redis

Это настроит три экземпляра Redis, работающих на разных портах. Мы будем использовать эти экземпляры для реализации консистентного хеширования и шардинга.

Шаг 3: Реализация сервиса сокращения URL

Давайте создадим наш основной файл приложения, index.js:

require('dotenv').config();
const express = require('express');
const redis = require('redis');
const shortid = require('shortid');

const app = express();
app.use(express.json());

const redisClients = [
  redis.createClient({ host: process.env.REDIS_HOST_1, port: process.env.REDIS_PORT_1 }),
  redis.createClient({ host: process.env.REDIS_HOST_2, port: process.env.REDIS_PORT_2 }),
  redis.createClient({ host: process.env.REDIS_HOST_3, port: process.env.REDIS_PORT_3 })
];

// Функция хеширования для распределения ключей между клиентами Redis
function getRedisClient(key) {
  const hash = key.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
  return redisClients[hash % redisClients.length];
}

// Конечная точка для сокращения URL
app.post('/shorten', async (req, res) => {
  const { url } = req.body;
  if (!url) return res.status(400).send('URL is required');

  const shortId = shortid.generate();
  const redisClient = getRedisClient(shortId);

  await redisClient.set(shortId, url);
  res.json({ shortUrl: `http://localhost:${process.env.PORT}/${shortId}` });
});

// Конечная точка для получения исходного URL
app.get('/:shortId', async (req, res) => {
  const { shortId } = req.params;
  const redisClient = getRedisClient(shortId);

  redisClient.get(shortId, (err, url) => {
    if (err || !url) {
      return res.status(404).send('URL not found');
    }
    res.redirect(url);
  });
});

app.listen(process.env.PORT, () => {
  console.log(`Server running on port ${process.env.PORT}`);
});

Как видно из этого кода, у нас есть:

  1. Консистентное хеширование:

    • Мы распределяем ключи (сокращенные URL) между несколькими клиентами Redis, используя простую хеш-функцию.

    • Хеш-функция гарантирует равномерное распределение URL-адресов по экземплярам Redis.

  2. Сокращение URL-адресов:

    • Эндпоинт /shorten принимает длинный URL и генерирует короткий идентификатор с использованием библиотеки shortid.

    • Сокращенный URL-адрес сохраняется в одном из экземпляров Redis с использованием нашей хэш-функции.

  3. Перенаправление URL-адресов:

    • Эндпоинт /:shortId извлекает исходный URL-адрес из кэша и перенаправляет пользователя.

    • Если URL не найден в кэше, возвращается ответ 404.

Шаг 4: Внедрение инвалидации кэша

В реальном приложении URL-адреса могут истечь или измениться со временем. Для обработки этого необходимо реализовать инвалидацию кэша.

Добавление истечения срока действия для кешированных URL-адресов

Давайте изменить наш файл index.js, чтобы установить время истечения для каждой кешированной записи:

// Конечная точка для сокращения URL с истечением срока действия
app.post('/shorten', async (req, res) => {
  const { url, ttl } = req.body; // ttl (время жизни) необязательно
  if (!url) return res.status(400).send('URL is required');

  const shortId = shortid.generate();
  const redisClient = getRedisClient(shortId);

  await redisClient.set(shortId, url, 'EX', ttl || 3600); // По умолчанию TTL 1 час
  res.json({ shortUrl: `http://localhost:${process.env.PORT}/${shortId}` });
});
  • TTL (Время жизни): Мы устанавливаем время истечения по умолчанию 1 час для каждого сокращенного URL-адреса. При необходимости вы можете настроить TTL для каждого URL-адреса.

  • Инвалидация кэша: При истечении TTL запись автоматически удаляется из кэша.

Шаг 5: Мониторинг показателей кэша

Для отслеживания попаданий в кэш и промахов мы добавим некоторое логирование в наши конечные точки в index.js:

app.get('/:shortId', async (req, res) => {
  const { shortId } = req.params;
  const redisClient = getRedisClient(shortId);

  redisClient.get(shortId, (err, url) => {
    if (err || !url) {
      console.log(`Cache miss for key: ${shortId}`);
      return res.status(404).send('URL not found');
    }
    console.log(`Cache hit for key: ${shortId}`);
    res.redirect(url);
  });
});

Вот что происходит в этом коде:

  • Попадания в кэш: Если URL найден в кэше, это попадание в кэш.

  • Промахи в кэше: Если URL не найден, это промах в кэше.

  • Это логирование поможет вам отслеживать производительность вашего распределенного кэша.

Шаг 6: Тестирование приложения

  1. Запустите ваши экземпляры Redis:
docker start redis1 redis2 redis3
  1. Запустите сервер Node.js:
node index.js
  1. Проверьте конечные точки с помощью curl или Postman:

    • Укоротить URL:

        POST http://localhost:3000/shorten
        Тело: "url": "https://example.com" }
      
    • Обратитесь по укороченному URL:

        GET http://localhost:3000/{shortId}
      

Вывод: Что вы изучили

Поздравляем! Вы успешно создали масштабируемый сервис сокращения URL-адресов с распределенным кэшированием с использованием Node.js и Redis. В ходе этого учебного пособия вы узнали, как:

  1. Реализуйте консистентное хеширование для распределения кэш-записей по нескольким экземплярам Redis.

  2. Оптимизируйте ваше приложение с помощью стратегий инвалидации кэша, чтобы данные были актуальными.

  3. Используйте Docker для моделирования распределенной среды с несколькими узлами Redis.

  4. Отслеживайте попадания и промахи кэша, чтобы оптимизировать производительность.

Следующие шаги:

  • Добавьте базу данных: Храните URL-адреса в базе данных для сохранения данных за пределами кэша.

  • Реализуйте аналитику: Отслеживайте количество кликов и аналитику для сокращенных URL-адресов.

  • Разверните в облаке: Разверните ваше приложение с использованием Kubernetes для автомасштабирования и отказоустойчивости.

Счастливого кодинга!