在本教程中,我们将使用 Node.js 和 Redis 构建一个可扩展的 URL 缩短服务。这项服务将利用分布式缓存来有效处理高流量,降低延迟,并实现无缝扩展。我们将探讨一些关键概念,如一致性哈希、缓存失效策略和分片,以确保系统保持快速和可靠。

在本指南结束时,您将拥有一个使用分布式缓存优化性能的全功能 URL 缩短服务。我们还将创建一个互动演示,用户可以在其中输入 URL,并查看缓存命中和未命中等实时指标。

您将学到什么

  • 如何使用 Node.jsRedis 构建 URL 缩短服务。

  • 如何实现 分布式缓存 以优化性能。

  • 理解 一致性哈希缓存失效策略

  • 使用 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:轻量级 Web 服务器框架。

  • redis:用于处理缓存。

  • shortid:用于生成短且唯一的 ID。

  • 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库生成短ID。

    • 缩短的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}` });
});
  • 存活时间(Time-To-Live):我们为每个缩短的URL设置了1小时的默认过期时间。如果需要,您可以为每个URL自定义TTL。

  • 缓存失效:当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
        Body: "url": "https://example.com" }
      
    • 访问缩短后的URL:

        GET http://localhost:3000/}
      

总结: 你学到了什么

恭喜!你已成功构建了一个使用 Node.js 和 Redis 实现 URL 缩短服务,具备 分布式缓存 的可伸缩性。在本教程中,你学会了如何:

  1. 实现一致性哈希以在多个Redis实例间分发缓存条目。

  2. 通过缓存失效策略优化您的应用程序,以保持数据更新。

  3. 使用Docker模拟具有多个Redis节点的分布式环境。

  4. 监控缓存命中和未命中以优化性能。

接下来的步骤:

  • 添加数据库:将URL存储在数据库中,以实现缓存之外的持久性。

  • 实现分析:跟踪点击次数和缩短URL的分析。

  • 部署到云端:使用Kubernetes部署您的应用程序,实现自动扩展和弹性。

祝您编码愉快!