news 2026/4/26 11:10:24

从零搭建一个Node.js短链接服务:我用Redis搞定了缓存和计数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零搭建一个Node.js短链接服务:我用Redis搞定了缓存和计数

从零搭建一个Node.js短链接服务:我用Redis搞定了缓存和计数

短链接服务是互联网中常见的基础设施,它能将冗长的URL压缩成简洁的短码,便于分享和传播。本文将带你从零开始,使用Node.js和Redis构建一个功能完善的短链接服务。我们将重点探讨如何利用Redis的多种数据结构特性,高效解决缓存、数据存储和访问统计等核心问题。

1. 项目初始化与环境准备

首先,我们需要创建一个新的Node.js项目并安装必要的依赖。打开终端,执行以下命令:

mkdir short-url-service cd short-url-service npm init -y npm install express redis nanoid

这里我们选择了几个关键依赖:

  • express:轻量级的Node.js web框架
  • redis:Node.js的Redis客户端
  • nanoid:用于生成短码的唯一ID生成器

接下来,创建一个基础的项目结构:

short-url-service/ ├── src/ │ ├── config/ │ │ └── redis.js │ ├── controllers/ │ │ └── url.controller.js │ ├── routes/ │ │ └── url.route.js │ └── app.js ├── .env └── package.json

2. Redis基础配置与连接管理

src/config/redis.js中,我们配置Redis连接:

const redis = require('redis'); const { promisify } = require('util'); const client = redis.createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379' }); client.on('error', (err) => { console.error('Redis连接错误:', err); }); // 将回调风格的Redis方法转换为Promise const asyncClient = { set: promisify(client.set).bind(client), get: promisify(client.get).bind(client), hset: promisify(client.hset).bind(client), hgetall: promisify(client.hgetall).bind(client), sadd: promisify(client.sadd).bind(client), smembers: promisify(client.smembers).bind(client), expire: promisify(client.expire).bind(client), incr: promisify(client.incr).bind(client) }; module.exports = { client, asyncClient };

这种配置方式有几个优点:

  1. 使用环境变量管理敏感配置
  2. 将回调API转换为更易用的Promise
  3. 集中管理所有Redis操作方法

3. 短链接核心功能实现

3.1 短码生成与URL映射

url.controller.js中,我们实现创建短链接的核心逻辑:

const { asyncClient } = require('../config/redis'); const { customAlphabet } = require('nanoid'); // 使用数字和字母生成6位短码 const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const generateShortCode = customAlphabet(alphabet, 6); async function createShortUrl(originalUrl) { const shortCode = generateShortCode(); // 使用Redis字符串存储短码到原始URL的映射,设置7天过期 await asyncClient.set(`url:${shortCode}`, originalUrl, 'EX', 60 * 60 * 24 * 7); // 使用Hash存储URL元数据 await asyncClient.hset(`url:meta:${shortCode}`, { originalUrl, createdAt: Date.now(), clicks: 0 }); return shortCode; }

这里我们同时使用了Redis的两种数据结构:

  • 字符串(String):存储短码到原始URL的直接映射,并设置过期时间
  • 哈希(Hash):存储更丰富的URL元数据,包括创建时间和点击次数

3.2 短链接重定向与访问统计

当用户访问短链接时,我们需要处理重定向并记录访问数据:

async function redirectShortUrl(req, res) { const { shortCode } = req.params; const ip = req.ip; // 检查IP是否在黑名单中 const isBlocked = await asyncClient.sismember('ip:blacklist', ip); if (isBlocked) { return res.status(403).send('Access denied'); } // 获取原始URL const originalUrl = await asyncClient.get(`url:${shortCode}`); if (!originalUrl) { return res.status(404).send('Short URL not found'); } // 更新统计数据 await Promise.all([ asyncClient.incr(`url:meta:${shortCode}:clicks`), // 增加总点击量 asyncClient.hincrby(`url:meta:${shortCode}`, 'clicks', 1), // 哈希中的点击量 asyncClient.sadd(`url:access:${shortCode}:ips`, ip) // 记录访问IP ]); res.redirect(originalUrl); }

这个实现展示了Redis的几种高级用法:

  1. 集合(Set):用于IP黑名单检查
  2. 字符串自增:高效实现计数器
  3. 哈希操作:更新结构化数据
  4. 集合记录:追踪唯一访问者

4. 高级功能实现

4.1 热门短链接排行榜

利用Redis的有序集合(Sorted Set)可以轻松实现热门链接排行:

async function getTopUrls(limit = 10) { // 更新所有URL的分数为当前点击量 const urlKeys = await asyncClient.keys('url:meta:*'); const pipeline = client.multi(); urlKeys.forEach(key => { const shortCode = key.replace('url:meta:', ''); pipeline.zadd('url:top', 0, shortCode); // 初始化分数为0 }); await pipeline.exec(); // 获取点击量最高的短码 const topUrls = await asyncClient.zrevrange('url:top', 0, limit - 1, 'WITHSCORES'); // 获取对应的原始URL const results = []; for (let i = 0; i < topUrls.length; i += 2) { const shortCode = topUrls[i]; const clicks = topUrls[i + 1]; const originalUrl = await asyncClient.get(`url:${shortCode}`); results.push({ shortCode, originalUrl, clicks: parseInt(clicks) }); } return results; }

4.2 短链接分析报表

我们可以利用Redis的多种数据结构生成详细的访问分析:

async function getUrlAnalytics(shortCode) { const [meta, accessIps, hourlyHits] = await Promise.all([ asyncClient.hgetall(`url:meta:${shortCode}`), asyncClient.smembers(`url:access:${shortCode}:ips`), asyncClient.hgetall(`url:access:${shortCode}:hours`) ]); return { shortCode, originalUrl: meta.originalUrl, createdAt: new Date(parseInt(meta.createdAt)), totalClicks: parseInt(meta.clicks), uniqueVisitors: accessIps.length, hourlyDistribution: hourlyHits }; }

5. 性能优化与最佳实践

在实现短链接服务时,有几个关键的性能考虑点:

  1. 短码生成冲突处理

    async function generateUniqueShortCode() { let attempts = 0; const maxAttempts = 5; while (attempts < maxAttempts) { const shortCode = generateShortCode(); const exists = await asyncClient.exists(`url:${shortCode}`); if (!exists) { return shortCode; } attempts++; } throw new Error('Failed to generate unique short code'); }
  2. 批量操作使用Pipeline

    async function batchCreateUrls(urls) { const pipeline = client.multi(); for (const url of urls) { const shortCode = generateShortCode(); pipeline.set(`url:${shortCode}`, url, 'EX', 60 * 60 * 24 * 7); pipeline.hset(`url:meta:${shortCode}`, { originalUrl: url, createdAt: Date.now(), clicks: 0 }); } return pipeline.exec(); }
  3. 内存优化建议

    • 为不同的数据类型设置适当的过期时间
    • 定期清理不再使用的数据
    • 对大集合考虑分片存储

在实际项目中,我发现最实用的优化是合理组合Redis的不同数据结构。比如使用Hash存储主要元数据,同时用独立的String存储最常访问的映射关系,这样可以在保证功能完整性的同时获得最佳性能。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/26 11:04:22

性能对决:用std::copy还是memcpy?实测告诉你POD类型数组拷贝谁更快

性能对决&#xff1a;用std::copy还是memcpy&#xff1f;实测告诉你POD类型数组拷贝谁更快 在C高性能计算和系统级编程中&#xff0c;内存拷贝操作的性能差异往往能决定整个系统的吞吐量。当我们需要拷贝一个基础数据类型的数组时&#xff0c;开发者常常面临一个选择&#xff…

作者头像 李华
网站建设 2026/4/26 10:55:11

显卡稳定性测试:使用MemTestCL快速诊断GPU内存问题

显卡稳定性测试&#xff1a;使用MemTestCL快速诊断GPU内存问题 【免费下载链接】memtestCL OpenCL memory tester for GPUs 项目地址: https://gitcode.com/gh_mirrors/me/memtestCL 还在为游戏闪退、设计软件崩溃而烦恼吗&#xff1f;你的显卡可能正在发出求救信号&…

作者头像 李华
网站建设 2026/4/26 10:53:21

技术解密:Noto Emoji 跨平台表情符号渲染架构

技术解密&#xff1a;Noto Emoji 跨平台表情符号渲染架构 【免费下载链接】noto-emoji Noto Emoji fonts 项目地址: https://gitcode.com/gh_mirrors/no/noto-emoji 在数字通信日益全球化的今天&#xff0c;表情符号已成为跨越语言障碍的重要沟通工具。然而&#xff0c;…

作者头像 李华