news 2026/8/5 15:04:36

从哈希算法到高并发架构:自建生产级URL短链服务全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从哈希算法到高并发架构:自建生产级URL短链服务全解析

在信息大爆发的今天,咱们每天跟各种链接打交道。不管是分享一篇深度好文、一个产品落地页,还是一次活动的报名入口,那些冗长、复杂又不美观的原始URL总是显得格格不入。它们不仅难记,在社交媒体、短信或印刷品上传播时,经常因为换行或者字符限制变得支离破碎,甚至影响品牌形象。这就是URL短链服务存在的核心价值:把一长串字符压缩成一个简短、易记、可追踪的标识符。

chhoto-url这个项目,听名字就知道,"chhoto"在孟加拉语和印地语里是“小”的意思。它的目标就是打造一个轻巧高效的URL短链服务。这可不是Bitly或TinyURL的简单复刻,而是从开发者角度出发,提供一个完全可控、容易部署,且能根据业务需求深度定制的解决方案。对于独立开发者、初创团队,或者需要在内部系统集成短链功能的企业来说,拥有一个自托管的短链服务意味着数据自主、功能灵活和成本可控。我之所以对这个项目感兴趣,是因为在实际工作中多次遇到类似需求:市场活动要追踪不同渠道的点击效果;内部系统生成的报告链接太长,影响邮件美观;API文档里需要分享可读性更高的示例链接。使用第三方服务虽然方便,但总会遇到免费额度限制、自定义域名收费、数据隐私顾虑,以及功能无法完全匹配业务逻辑这些问题。自己动手搭建一个,虽然前期需要一些投入,但从长远看,其灵活性、可控性和学习价值是无可替代的接下来,我将从设计思路到代码实现,完整拆解如何构建一个像chhoto-url这样的生产级短链服务。

构建一个短链服务,远不止是生成一个随机字符串映射到长URL那么简单。它需要综合考虑高并发低延迟数据持久化、防滥用以及可扩展性。chhoto-url项目采用了一种经典且稳健的微服务架构思想,将不同关注点分离到独立的模块中。一个完整的短链服务通常包含以下几个核心组件:

1. API服务层:接收创建查询重定向请求,是系统的门面

2. 短码生成器:核心算法所在,负责将长URL转换为唯一的短字符串

3. 数据存储层:持久化短码与长URL的映射关系

4. 重定向引擎:实现高性能的302/301跳转

5. 管理控制台(可选):用于查看统计数据、管理链接

chhoto-url的架构倾向于轻量化和一体化。它可能使用一个单一的Web应用框架(如Node.js的Express、Python的Flask/FastAPI、Go的Gin)来同时处理API和重定向逻辑。这种选择对于中小规模、快速启动的项目来说,简化了部署和运维复杂度。

关键技术选型背后的思考,后端语言与框架方面,项目选用Node.js与Express框架的可能性很大。原因在于非阻塞I/O与高并发:短链服务的重定向操作是典型的I/O密集型场景(主要是数据库查询)Node.js的异步特性非常适合处理大量并发的小请求,能够用较少的资源支撑较高的QPS(每秒查询率)。还有开发效率,JavaScript生态统一,Express框架轻量且中间件机制灵活可以快速搭建RESTful API。当然,如果追求极致的性能与内存效率,Go (Gin/Echo) 是更优的选择;如果团队熟悉Python,FastAPI也能提供非常出色的开发体验和性能。选型的核心是匹配团队技术栈和性能预期。

数据存储是设计的重中之重。我们需要一个能快速根据短码(Key)查找到长URL(Value)的存储系统。

关系型数据库(如PostgreSQL, MySQL):结构清晰,易于做数据分析(如统计点击量、来源)。可以通过给短码字段添加唯一索引来保证唯一性和查询速度。但对于纯KV查询,略显重量

键值数据库(如Redis):这是短链服务的“黄金搭档”。所有数据常驻内存,读写性能极高(微秒级),完美匹配GET /:shortCode这种高频查询操作chhoto-url很可能将Redis作为核心存储或缓存层。

混合方案:一种生产环境常见模式是“Redis作缓存,SQL作持久化”。新创建的短链写入SQL数据库,并同时加载到Redis。查询时先查Redis,未命中再查SQL并回填Redis。这既保证了速度,又保证了数据可靠性。

短码生成算法:这是短链服务的灵魂,需要平衡冲突概率、长度与可读性以及安全性。

1. 哈希算法(如MD5, SHA-1) + 进制转换

操作:对长URL计算哈希值,取前若干位(如8个字节),将其转换为62进制(a-zA-Z0-9)字符串。

优点:同一URL始终生成相同短码,可实现去重。

缺点:可能存在哈希冲突(虽然概率极低),需要检测并处理;生成的短码是随机的,无顺序。

2. 分布式ID生成器(如Snowflake算法) + 进制转换

操作:生成一个全局递增的唯一ID(如64位整数),将其转换为62进制字符串。

优点:绝对唯一,短码长度可预测且有序。

缺点:同一URL多次创建会得到不同短码,无法去重;需要维护ID生成服务。

3. 预生成随机码池

操作:服务启动时或后台任务预先生成一大批随机、唯一的短码存入数据库“待使用”状态。创建短链时,直接从池中取一个,标记为“已使用”。

优点:创建操作极快(SELECT ... FOR UPDATE或LPOP),避免了实时生成的算力消耗。

缺点:需要管理码池的补充逻辑;无法根据URL去重。

chhoto-url项目为了简单起见,很可能采用第一种方案(哈希+进制转换),并在创建时进行冲突检测和重试。这是一种在简单性和可靠性之间取得良好平衡的方案。

注意:绝对不要使用自增ID直接暴露为短码(如/1, /2)。这会导致严重的安全问题,他人可以轻易遍历所有链接,并可能通过ID推测出业务量。

让我们抛开抽象的架构图,深入到代码层面,看看每个核心模块具体如何实现。我将以 Node.js + Express + Redis 的技术栈为例进行说明,这种组合在实现轻量级服务时非常高效

短码生成器的实现

我们选择“哈希(SHA-256) + 截断 + 62进制转换”的方案。SHA-256冲突概率极低,足以应对民用场景。

// utils/shortCodeGenerator.js

const crypto = require('crypto');

const BASE62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

const CODE_LENGTH = 7; // 生成7位短码,62^7 ≈ 3.5万亿种组合

/**

* 生成短码

* @param {string} longUrl - 原始长链接

* @param {number} [start] - 哈希值截取起始位置(用于冲突重试)

* @returns {string} 短码

*/

function generateShortCode(longUrl, start = 0) {

// 1. 计算SHA-256哈希

const hash = crypto.createHash('sha256').update(longUrl).digest('hex');

// 哈希值为16进制字符串,例如 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

// 2. 截取部分哈希值(这里取8个字符,即4字节)

const hashSubset = hash.substring(start, start + 8);

// 3. 将16进制字符串转换为大整数

let num = BigInt('0x' + hashSubset);

// 4. 转换为62进制

let code = '';

while (num > 0) {

const remainder = Number(num % 62n);

code = BASE62[remainder] + code;

num = num / 62n;

}

// 5. 补齐长度到CODE_LENGTH,用‘a’填充(或随机字符填充更佳)

while (code.length < CODE_LENGTH) {

code = BASE62[0] + code; // 用‘a’填充左侧

}

// 6. 取后CODE_LENGTH位(因为左侧填充可能导致超长)

return code.slice(-CODE_LENGTH);

}

/**

* 创建短链,处理冲突

* @param {string} longUrl

* @param {Function} checkExists - 检查短码是否已存在的函数

* @returns {Promise} 短码

*/

async function createShortCode(longUrl, checkExists) {

let attempts = 0;

const MAX_ATTEMPTS = 5; // 最大重试次数

let shortCode;

while (attempts < MAX_ATTEMPTS) {

// 每次尝试从哈希值的不同位置开始截取

shortCode = generateShortCode(longUrl, attempts * 2);

const exists = await checkExists(shortCode);

if (!exists) {

return shortCode;

}

attempts++;

console.warn(短码冲突: ${shortCode}, 进行第${attempts}次重试);

}

throw new Error('无法生成唯一短码,请稍后重试');

}

module.exports = { generateShortCode, createShortCode };

实操要点

短码长度:7位短码(62^7 ≈ 3.5万亿)对于个人或中小型项目完全够用。如需更多,可增至8位。

冲突处理:createShortCode函数通过偏移截取位置来生成不同的短码进行重试,这是一种简单有效的策略。

填充策略:上述填充用‘a’,可能导致短码分布不均。更优的做法是用随机字符填充,或直接取哈希值转换后字符串的前N位。

数据存储与缓存策略

我们采用“Redis为主,MySQL为辅”的混合模式。Redis存储热点映射和点击计数,MySQL持久化所有数据用于备份和分析。

// models/urlModel.js

const redis = require('redis');

const { promisify } = require('util');

const mysql = require('mysql2/promise'); // 使用mysql2的Promise接口

// 配置连接

const redisClient = redis.createClient({ url: 'redis://localhost:6379' });

const redisGetAsync = promisify(redisClient.get).bind(redisClient);

const redisSetexAsync = promisify(redisClient.setex).bind(redisClient);

// MySQL连接池

const mysqlPool = mysql.createPool({

host: 'localhost',

user: 'root',

password: 'password',

database: 'shortener_db',

waitForConnections: true,

connectionLimit: 10,

queueLimit: 0

});

class UrlModel {

// 1. 创建短链

async createMapping(shortCode, longUrl, creatorIp = '') {

const now = new Date();

const mysqlConn = await mysqlPool.getConnection();

try {

await mysqlConn.beginTransaction();

// 写入MySQL

const [result] = await mysqlConn.execute(

INSERT INTO url_mappings (short_code, long_url, creator_ip, created_at) VALUES (?, ?, ?, ?),

[shortCode, longUrl, creatorIp, now]

);

// 写入Redis,设置过期时间(例如30天)

await redisSetexAsync(url:${shortCode}, 30 24 3600, longUrl);

// 初始化点击量缓存

await redisSetexAsync(clicks:${shortCode}, 30 24 3600, '0');

await mysqlConn.commit();

return { id: result.insertId, shortCode, longUrl };

} catch (error) {

await mysqlConn.rollback();

// 如果发生错误,尝试清理可能已写入的Redis数据

redisClient.del(url:${shortCode}, clicks:${shortCode});

throw error;

} finally {

mysqlConn.release();

}

}

// 2. 根据短码查找长链接(重定向时调用)

async getLongUrl(shortCode) {

// 首先查询Redis缓存

let longUrl = await redisGetAsync(url:${shortCode});

if (longUrl) {

// 异步更新点击量,不阻塞重定向

this._incrementClicksAsync(shortCode);

return longUrl;

}

// 缓存未命中,查询MySQL

const [rows] = await mysqlPool.execute(

SELECT long_url FROM url_mappings WHERE short_code = ?,

[shortCode]

);

if (rows.length > 0) {

longUrl = rows[0].long_url;

// 回填Redis缓存,设置较短过期时间(如1小时),因为这是冷数据

await redisSetexAsync(url:${shortCode}, 3600, longUrl);

// 异步更新点击量

this._incrementClicksAsync(shortCode);

return longUrl;

}

return null; // 未找到

}

// 3. 异步增加点击量

async _incrementClicksAsync(shortCode) {

// 使用Redis INCR命令原子性增加计数

redis

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

Ostrakon-VL-8B实测:如何把15秒的卡顿优化到5秒以内?分享我的避坑指南

Ostrakon-VL-8B实操手册:推理时间5-15秒波动原因分析与图片分辨率优化策略1. 引言:从一次让店员想砸手机的测试说起上周我在一家连锁超市进行Ostrakon-VL-8B的实地部署测试时遇到了一个特别尴尬的场景。当时我对着货架拍了一张照片,想让模型帮我分析商品陈列情况。结果,第一…

作者头像 李华
网站建设 2026/8/5 15:04:02

零门槛搞定VMware硬核科普:我在快马上用代码写出的“虚拟机模拟器”,小白也能玩出花

说实话,刚接触虚拟化技术那会儿,我心里其实是打怵的。那时候总觉得,什么CPU指令集、内存分页、网卡桥接,这些词儿听起来就像是天书,离我这个只会写点简单脚本的“门外汉”太遥远了。脑子里对虚拟机的印象还停留在“装个系统跑跑看”的朴素阶段,根本不知道怎么去拆解里面的…

作者头像 李华
网站建设 2026/8/5 15:02:53

如何优雅捕获网页媒体?猫抓浏览器扩展的三大创新解决方案

如何优雅捕获网页媒体&#xff1f;猫抓浏览器扩展的三大创新解决方案 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 你是否曾经面对心仪的视频教程…

作者头像 李华
网站建设 2026/8/5 15:01:47

上海市建设局网站怎么查资质?老建筑工手把手教你避开陷阱,看懂证书背后的门道

说实话,在这个行业里摸爬滚打这些年,我见过太多因为“信息不对称”吃大亏的老板和项目经理了。以前咱们干工程,讲究的是人脉,是熟人介绍,谁家班子硬,谁家关系铁,活儿就往谁家送。但现在不一样了,时代变了,规矩也变了。尤其是随着数字化转型的推进,所有的监管都已经搬…

作者头像 李华
网站建设 2026/8/5 15:00:47

告别繁琐代码!用Mendix低代码+AI二维码工坊,打造零售库存管理神器

说到开发,咱们很多非科班出身的业务专家或者初级开发者,最怕的就是那种“纯技术”的东西。比如,你要给公司做一个仓库管理系统,老板拍着桌子说:“每个商品都要贴个码,扫一下就能看库存,还能自动更新,下周就要上线!”听到这话,你心里是不是“咯噔”一下?心想这得写多…

作者头像 李华
网站建设 2026/8/5 15:00:23

老Mac续命秘籍:OpenCore Legacy Patcher 手把手教你让闲置机型重获新生

说真的,手里攥着一台两三年前淘汰下来的Intel Mac,看着它吃灰,心里总不是滋味。那种感觉就像是收藏了一张绝版黑胶唱片,舍不得扔,又找不到唱机。直到我遇到了 OpenCore Legacy Patcher(以下简称 OCLP),这玩意儿简直就是我们这些“垃圾佬”或者说是对苹果生态有情怀用户…

作者头像 李华