ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从零自研短链系统:从短码生成到高并发跳转的完整实战

2026/9/20 8:44:51 拓冰建站 浏览量
从零自研短链系统:从短码生成到高并发跳转的完整实战 1. 内容整体设计与思路拆解短链这东西听起来是个再小不过的工程。做之前我也觉得不就是把一串长 URL 变成一个小短码嘛能有多复杂。可真到动手写的时候才发现一个能上线的短链系统几乎把后端常见的套路全部串起来了编码算法、缓存设计、数据库索引、跳转协议、安全校验、异步统计甚至还有一点风控思维。Day04 复习到这儿我把整套设计重新拉通了一遍感觉不写下来是真的亏。你在电商 App、内容平台、营销短信里点到的链接十个里面有八个都是短链。有些短链是平台内部跳转用的有些是分享给外部用户的。大家平时刷小红书、逛淘宝看到的商品链接就是典型场景——正文里只能放有限内容而真实落地页可能有几十个参数不带短链的话链接又长又容易被截断。更别提用户对“跳转链接”天然有警惕心什么“淘宝短链跳第三方是真的吗”这类疑问在各大社交平台隔一阵就会冒出一次。这其实侧面说明了一件事短链系统不仅要保证“跳得过去”还要保证“跳得明白、跳得安全”。所以我把这次短链项目复习的核心目标拆成四块一是把长链变短这是表面功夫二是把跳转做强也就是高并发下还能稳定快速响应三是把统计做全记录谁在什么时间、用什么设备点了这个链接四是把安全做好避免链接被拿来钓鱼、跳转外站、或者被刷量。这四个目标环环相扣缺一个都不能算完整的短链服务。1.1 为什么选择“自研短链”而不是直接买服务市面上的短链服务不少有免费的也有收费的功能看着也齐全。但个人项目想真正练手自研和买服务完全是两个量级的事。买服务你只需要调用 API拿回来一个短链直接用省事是省事但对内部机制一无所知。比如短码怎么生成、碰撞怎么处理、跳转状态码该用 301 还是 302、统计日志怎么设计这些细节全部被黑盒封装了。写个人项目如果只停留在“会用”那跟背单词只背了词形没背词义一样等于没学。自研短链还有一个实际收益你可以完全掌控链路。很多短链服务会插入自己的广告页或者中间跳转页这有时候会影响用户体验。自己做的话从生成短码到落地页跳转中间哪些环节需要展示提示页哪些流量走静默 302都由你来定。另外自研后数据全部在自己手里点击量、来源渠道、设备分布想怎么分析就怎么分析不会被第三方服务的数据口径限制住。当然自研也不是什么都自己做。像 Redis、MySQL、Nginx 这些基础设施直接用开源方案就行没必要重复造轮子。整个项目的自研核心其实只在业务层短码生成算法、跳转控制器、统计落库逻辑、安全校验模块。把这些做透了短链系统的骨架也就立起来了。1.2 短码生成方案对比自增 ID、哈希截取、预生成随机码短码生成是整个系统的起点。表面上看就是“把一个数字/字符串变成短码”但选什么方案直接影响系统后续的扩展性、安全性和维护成本。我复习时对比了三种主流方案也踩过其中的坑在这里给各位复盘一下。方案一自增 ID 转 62 进制。数据库自增主键拿到一个十进制 ID然后把这个 ID 转换成 62 进制字符串0-9a-zA-Z 共 62 个字符长度随 ID 增大而变长。这个方案实现最简单ID 天然唯一不需要额外处理碰撞而且还能从短码反推生成顺序。缺点也很明显短码可预测别人可以通过遍历短码把所有公开链接都扫出来这对某些私密链接来说是灾难。而且如果直接用数据库的自增主键那主键本身就暴露了业务量有点不体面。方案二对原始 URL 做哈希截取。用 MD5、SHA-256 之类的算法对长 URL 求哈希然后截取前 6~8 位作为短码。这个方案的好处是同一个长 URL 理论上会得到同一个短码天然支持去重省存储。坏处是哈希碰撞没法完全避免需要查重和二次处理截断后随机性也不够好分布不均匀而且面对恶意用户构造的批量长链接哈希计算成本也不低。方案三预生成随机码并入库。维护一张短码池表提前生成一批随机的短码使用时取一个就标记为已占用。短码随机性高不容易被遍历猜测安全性最好。但实现复杂度最高需要维护池子的补充逻辑并发取码还要考虑锁竞争。我自己最终采用的是方案一为主、方案二为辅的折中做法数据库自增 ID 不直接对外而是先做一次位混淆比如乘以一个大质数再取模再把混淆后的数字转 62 进制。这样既保持了唯一性又让短码无法轻易被按顺序遍历——你拿到一个短码也没法推断出前一个短码指向的链接。这个做法在个人项目里性价比很高既不用维护短码池又能挡住 90% 的瞎扫行为。1.3 跳转状态码选型301 还是 302二者差别不小短链的跳转动作核心是给浏览器返回一个 HTTP 重定向。但用 301 还是 302不是随手一选的事网上很多教程根本不提这个细节导致不少人上线后才发现统计数据对不上、链接被“永久”缓存在了用户浏览器里。301 是永久重定向。你访问短链服务器返回 301告诉浏览器“这个链接以后永远指向这个目标地址”浏览器就会把这个映射关系缓存下来。下次再访问短链浏览器直接跳过服务器自己去目标地址连请求都不发给短链服务了。这会导致一系列问题用户第二次点击不再触发统计因为请求到不了你的服务器就算你后续修改了短链指向的目标地址用户浏览器里缓存的还是旧地址改了好几次都没生效。所以 301 基本只适合那种“一个短码绑定一个永久地址、完全不需要统计”的场景实战里极少用。302 是临时重定向。服务器告诉浏览器“这次先跳到这个地址下次你还来问我”。这样每次点击都会先请求短链服务才能跳转统计数据自然就全了。用户第一次访问是 A 链接你运营想改成 B 链接也只需要改数据库里那一条记录用户再点短链自然走到 B没有缓存干扰。这个选择题我在初版里就做错了。当时图省事直接返回 301结果统计面板里点击量比实际少了一大截而且怎么查都查不出问题最后用 curl 抓包才发现是浏览器把重定向缓存了。大家务必记住短链系统默认用 302只有在明确“链接地址永不变、不需要统计”的场景才考虑 301。2. 核心细节解析与实操要点把整体方案定下来之后真正的工作才开始。短链系统最烦的不是“想不到”而是“想到了但做不细”。这一步我按模块逐个拆解把每个环节的关键参数和注意事项都盘了一遍。2.1 短码生成算法的参数推导与转换过程用自增 ID 转 62 进制这个方案需要先把 62 进制转换彻底搞明白。别觉得这就是“进制换算”小学知识真写起来字符集的排序、处理 0 的方式、长度的边界条件都是有讲究的。先定字符集。一般用这个顺序0-9对应十进制 0~9、a-z对应 10~35、A-Z对应 36~61。注意不同项目可能用不同的字符集顺序比如有的把大小写顺序反过来有的把部分形近字符去掉了。这个字符集本身没有绝对标准但选定之后要全局统一尤其是后续要做短码反解时映射表一错就全乱了。接下来是转换逻辑。假设数据库自增 ID 是45678901先做一次位混淆。我这里用的混淆方式是seed (ID * 9301 49297) % 233280这是线性同余法生成伪随机数时用的一组经典参数放在这里能让连续 ID 的短码看起来完全不相关。当然如果你不想混淆直接转也行但推荐加一层毕竟短码直接暴露自增序号的成本很低。混淆后的数记为n接下来循环执行chars 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ result while n 0: result chars[n % 62] result n n // 62拿45678901 * 9301 49297举例先算混淆值45678901*9301 424860691801再加49297得到424860741098然后对233280取模得到203434左右具体值取决于参数我这里直接给思路你跑一下代码就拿到了。把203434转 62 进制203434 / 62 3281余数12对应字符c3281 / 62 52余数57对应字符57 - 10 10数一下字符集a是第 10 个往后数 57 位落在F附近具体看索引52 / 62 0余数52对应字符Q最终短码可能是cQF一类的三段字符串。这个过程你可以写成单元测试用已知 ID 验证转换是否稳定。实测下来7 位短码能覆盖 62^7 约 3.5 万亿个组合个人项目用到天荒地老也用不完。2.2 数据表设计短码、原链接和统计字段怎么安排存储层是短链的地基。表设计不合理后面做统计、做筛选都会掣肘。我这里用 MySQL 做演示设计了三张核心表短链映射表、访问日志表、域名白名单表。短链映射表的核心字段是这些字段类型说明idbigint 自增主键内部 ID用于生成短码short_codevarchar(16) 唯一索引短码对外暴露original_urlvarchar(2048)原始长链expire_timedatetime 可空过期时间空表示永久statustinyint状态1 正常 0 下线 2 审核中creator_idvarchar(64) 可空创建者标识created_atdatetime创建时间updated_atdatetime更新时间注意original_url一定要用varchar(2048)而不是varchar(255)很多长链接带了一堆查询参数255 长度根本不够。如果空间紧张还可以考虑用TEXT类型但建索引要提前算好前缀长度。short_code上加唯一索引是必须的一方面查询快另一方面数据库层面兜底防止碰撞。访问日志表用来记录每个短链的每一次点击字段类型说明idbigint 自增流水 IDshort_codevarchar(16)短码click_timedatetime点击时间ipvarchar(64)用户 IPIPv6 也够存user_agentvarchar(512)浏览器 UAreferervarchar(512)来源页device_typetinyint设备类型1 移动 2 PC 3 其他这张表的数据量会暴涨所以不用设太多索引主键 短码索引就够了。查询统计时按short_code click_time分组提前建好复合索引能快不少。域名白名单表存的是允许跳转的目标域名用于安全校验后面会细说。2.3 缓存设计为什么用 Redis、怎么防止穿透和击穿在高并发场景下如果每个跳转请求都打到 MySQL再好的数据库也会被拖垮。跳转这个动作的特点是读多写少而且读的是同一个短码映射天然适合加缓存。我用 Redis 做缓存层缓存 key 直接是短码value 存序列化后的目标 URL 和过期时间。缓存读取逻辑很简单用户访问短码先查 Redis查到了直接构造 302 跳转查不到就去 MySQL 捞捞到了回填 Redis再跳转MySQL 也没有就返回 404 页面。这里有几个实操要点。一是缓存过期时间要加“随机抖动”比如基础过期时间 24 小时再加 0 到 6 小时内的随机值。不然所有短链在同一时间点集中过期如果热点高瞬间所有请求都打回源数据库压力一下就上来了这就是缓存雪崩的常见触发场景。二是要防缓存穿透。恶意用户拿一堆不存在的短码来刷请求每次都不命中缓存直接打 MySQL数据库早晚被问崩。我用的处理方案是MySQL 查不到也缓存一个空值过期时间设短一点比如 5 分钟这样同一批不存在短码的重复请求会被缓存挡住不会再穿透下去。更正统的做法是引入布隆过滤器把所有存在的短码加载进去判断不存在就直接返回不走缓存也不走 DB。个人项目初期用空值缓存就够布隆过滤器等到了每日千万级访问再加不迟。三是回填缓存的时候要注意并发问题。两个请求同时来查同一个短码MySQL 都没命中缓存两边都去 DB 查并回填虽然不会出错但白白浪费了一次 DB 查询。这里可以用 Redis 的SETNX做互斥锁只有拿到锁的请求才允许查 DB其余请求先短暂 sleep 再重新读缓存能把重复回源降到最低。2.4 跳转安全与“第三方跳转提示页”的设计思路文章开头提到用户对“短链跳转”有顾虑甚至有人专门发帖问“淘宝短链跳第三方是真的吗”。从技术角度看平台在跳转前展示一个“安全提示页”或者说“中间页”恰恰是负责任的做法。有了中间页用户能看清楚自己即将去哪儿、由谁提供这个页面决定权握在自己手里而不是被一个短链蒙着眼睛拖到陌生地方。自研短链系统想做这个能力可以在跳转控制器里加一道校验。首先拿到目标 URL 后解析出域名拿这个域名到白名单表里比对。白名单命中就直接 302 跳转不命中就返回一个风险提示 HTML 页面上面写明目标地址用户点击“继续访问”才真正跳转同时记录一条“需确认跳转”的日志便于事后审计。这个设计还有一个好处能防范 Open Redirect 漏洞。所谓 Open Redirect就是短链被人利用把用户引导到钓鱼页面。比如有人提交了一个看似正常的链接但中间用 URL 编码、重定向嵌套等手段最终把人带到诈骗网站。处理办法是对目标 URL 做完整解析提取 scheme、host、port校验是否在允许名单里对 URL 编码做一次解码后再解析防止%2F%2Fevil.com这种绕过的花招如果目标域名是 IP 地址直接拦掉不放过任何直连 IP 的情况。我在项目里还加了一个“过期下线”的能力。如果某条短链被举报或自动检测判定为风险链接管理员可以直接将status置为 0顺便删掉 Redis 缓存。这一招在应对“短链被恶意使用”时非常管用不需要动代码运营同学在后台点一下按钮就能处理。3. 实操过程与核心环节实现理论学习再多不动手写代码都是纸上谈兵。这一节我把整个链路从代码层面走一遍给出可以直接参考的伪代码和核心逻辑。语言我选了 Python个人项目开发快但换成 Go 或 Java 思路完全一致。3.1 生成短码的核心实现首先生成短码的部分。我要做的是接收原始 URL 以及可选的过期时间生成短码并写入数据库。伪代码如下import time import hashlib CHARS 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ def id_to_short_code(seed: int) - str: if seed 0: return CHARS[0] code [] while seed 0: seed, rem divmod(seed, 62) code.append(CHARS[rem]) return .join(reversed(code)) def obfuscate_id(raw_id: int) - int: # 乘一个大质数再加偏移把连续 ID 打散 return (raw_id * 9301 49297) % 233280 def create_short_url(original_url: str, expire_timeNone): # 这里省略数据库连接细节 insert_id insert_mapping_record(original_url, expire_time) short_code id_to_short_code(obfuscate_id(insert_id)) update_short_code(insert_id, short_code) # 写入缓存 redis_client.setex(fshort:{short_code}, TTL, original_url) return short_code需要注意一个操作顺序先插入记录拿到自增 ID再算出短码再回填短码字段。为什么不等短码算出来再插因为短码依赖自增 ID而自增 ID 只能由数据库生成。中途如果插入失败短码字段留空没有关系后续重算即可。为了不让短码字段出现 NULL也可以在表设计时先给一个默认占位值拿到 ID 后立刻更新。3.2 跳转处理链路从缓存到回源再到 302跳转是短链系统最核心的接口也是响应时间最敏感的环节。我的处理流程大致如下def redirect_handler(short_code: str): # 1. 先查 Redis cache_value redis_client.get(fshort:{short_code}) if cache_value: if cache_value NULL: return 404 Not Found # 2. 命中缓存直接 302 return redirect_302(cache_value) # 3. 回源 MySQL record query_mapping_record(short_code) if not record: # 4. 空值缓存防止穿透 redis_client.setex(fshort:{short_code}, 300, NULL) return 404 Not Found # 5. 检查状态与过期时间 if record[status] ! 1 or is_expired(record[expire_time]): return 410 Gone # 6. 回填缓存设置随机过期时间 ttl 86400 random.randint(0, 21600) redis_client.setex(fshort:{short_code}, ttl, record[original_url]) # 7. 异步记录访问日志 async_log_click(short_code, request_ip, user_agent) # 8. 安全校验判断是否直接跳转 if is_domain_in_whitelist(record[original_url]): return redirect_302(record[original_url]) else: return render_redirect_confirm_page(record[original_url])这里有个容易被忽略的细节空值缓存的 key 一定要和正常缓存的 key 区分开或者在 value 里做标记否则下次拿到“NULL”这个字符串会当成要跳转的地址直接给用户返回一个试图跳转到/NULL的非法响应。异步记录访问日志我用了简单的方式把点击事件扔进 Redis 的 List 队列后台再起一个 worker 批量消费并写入 MySQL。这样一个请求不需要等待数据库写入完成响应时间能压得更低。如果怕日志丢失可以对 List 做持久化或者直接用消息队列代替。3.3 统计闭环日志聚合和展示维度统计日志只有聚合起来看才有意义。我每天凌晨跑一个定时任务把昨天的访问日志按短码、小时、设备类型做聚合写入统计结果表。SQL 大致是INSERT INTO click_stat (short_code, stat_date, hour, device_type, cnt) SELECT short_code, DATE(click_time), HOUR(click_time), device_type, COUNT(*) FROM click_log WHERE click_time 2025-01-01 00:00:00 AND click_time 2025-01-02 00:00:00 GROUP BY short_code, DATE(click_time), HOUR(click_time), device_type;统计结果表单独存在的意义是前端展示的时候不需要再去扫全量日志表。一个短链如果被扫了几百万次日志表重得不得了每次打开详情页都实时聚合数据库也扛不住。日级聚合的延迟虽然有点久但对于大部分营销链路分析场景已经足够。展示面板里我最关注的几个维度是总点击量、独立访客数按 IP 去重或按 Cookie 去重、点击时间分布24 小时热力图、设备占比、来源 Referer Top10。这套看板做完之后运营就能直观了解到某个活动链接在哪个时段、哪类设备上表现最好对投放决策有实际参考价值。3.4 部署环境与容量估算参考短链服务部署上我用的是一套非常经典的组合Nginx 做流量入口和静态资源处理后端服务跑在 Docker 容器里MySQL 存映射和日志Redis 做缓存和队列。Docker Compose 可以一键起整套服务开发环境和生产环境行为保持一致省去很多环境不一致导致的头疼问题。容量估算方面我给自己定的基准是系统每天新增 10 万个短链单条短链平均被访问 100 次。那么一年下来短链映射表大约 3650 万条日志表大约 36.5 亿条。映射表单表完全扛得住但日志表必须分表或按天分区否则查询回来越来越慢。缓存按 30 天有效计算假设活跃短链 3000 万条每条缓存占 200 字节左右Redis 需要预留 6GB 左右的内存实例规格不能低于这个数。如果你拿这些参数做压测就能提前知道自己的机器水位不至于线上撑爆了才反应过来。4. 常见问题与排查技巧实录短链系统写出来容易真正稳定跑起来问题一个接一个。我把实际开发中踩过并且修复过的典型问题整理成清单供你对照排查。这些问题在官方文档里基本找不到现成答案都是“掉过坑才知道”的类型。4.1 短码碰撞问题日志里出现奇奇怪怪的 404短码碰撞的本质是“两条不同的原链接生成了同一个短码”。如果用了自增 ID 方案理论是不会碰撞的但一旦混淆参数设得不合理或者回填短码的逻辑在并发下有覆盖风险就可能出现。我在本地压测时遇到过连续插入两条记录都拿到了短码更新语句的相同值后一条把前一条的 short_code 覆盖了。排查方式是先看数据库有没有重复索引报错再用日志关联创建时间和短码基本能定位到是回填逻辑缺了条件。解决办法是在short_code上建唯一索引同时更新语句加上WHERE short_code 或WHERE short_code IS NULL这样的条件防止重复更新。万一出现碰撞业务上可以临时生成一个随机后缀拼在短码后面。提示短码的生成逻辑写完后一定要做一次并发单元测试并发插入 1000 条记录看有没有唯一索引冲突。这个测试能省掉你后面很多排查时间。4.2 恶意刷量如何区分“真流量”和“假点击”上线没多久我就发现某条短链的点击量一夜之间暴涨了十几万一看 IP 全是同一个段很明显是被脚本刷了。刷量不仅污染统计结果还会白白消耗数据库和带宽资源。我的应对手段有三层。第一层是 IP 频率限制同一个 IP 在 1 分钟内最多访问 100 次短链超出直接返回 429并在 Redis 里记录违规 IP。第二层是 User-Agent 检测识别常见的爬虫库特征比如 Python-requests、curl 的 UA 指纹对这些请求返回正常的 302但打上“疑似机器”标签不计入正常的点击统计。第三层是对外提供签名的短链只有带合法签名的请求才被统计进入核心指标不过这个做起来偏重适合有更高安全诉求的场景。4.3 302 vs 301 导致的统计差异这个问题前面提过但值得再强调一次因为它太隐蔽了。如果你用了 301浏览器会把跳转结果永久缓存后续点击根本不会请求短链服务器。你的服务端日志上可能显示一分钟前刚有人访问过某个短链可实际上用户一小时内点了十次只有第一次到了你这里。排查技巧是用浏览器的无痕窗口访问短链打开开发者工具看网络请求如果看到状态码是301 Moved Permanently那就说明你的跳转写成了永久重定向。把代码里的 301 改成 302再清一次浏览器缓存统计数据就恢复正常了。4.4 链接被用于钓鱼必须做域名白名单和内容检测短链天生容易被滥用因为它把真实地址藏起来了。别人拿你的短链去钓鱼跳转到仿冒登录页用户被骗后只会怪这个短链平台。所以在上线之前我把目标域名的检测做好了创建短链时解析目标 URL 的域名跟一份常见的恶意域名黑名单比对黑名单命中的直接拒绝创建。同时对目标域名不在白名单里的链接跳转时都展示中间确认页宁可牺牲一点转化率也不能让用户无感知地被带走。淘宝、小红书这类平台在跳转第三方链接前的那个“安全提示页”本质上就是同一套思路。它不是为了恶心用户而是在短链的“隐藏性”和用户“知情权”之间找一个平衡点。自研系统如果要做大这个能力必须尽早纳入。4.5 Redis 缓存持续穿透布隆过滤器到底该不该上前面说过空值缓存能解决大部分穿透问题但它的缺陷是每次查一个不存在的短码都会先占用一份缓存空间而且存在短码字典量很大的时候空值缓存的数量会非常可观。布隆过滤器的做法是启动时把所有合法短码加载到一个 bitmap 中判断“一定不存在”直接返回判断“可能存在”再去查缓存和 DB。它的问题是存在误判率会把少部分非法请求放进真实查询链路但整体性能远优于空值缓存。我的建议是初期用空值缓存等合法短码数量超过百万级别且恶意请求占比确实影响了 DB 健康度再引入布隆过滤器。加布隆过滤器要注意一点当新增短码时除了写 DB别忘了同步把短码加入 bitmap不然新生成的短链会被自己的过滤器挡住那乐子就大了。4.6 短链过期时间到了Redis 和 MySQL 数据不一致怎么办我给部分短链设置了过期时间比如 7 天后失效。第一个版本里只在跳转时判断expire_time过了时间就返回 410但 Redis 缓存还在导致每次都要查 MySQL 才能知道到底过没过期。后来做了优化写入缓存时TTL 等于expire_time - now让缓存自动过期回源后继续判断过期状态。但这里有个坑如果用户修改了短链的过期时间比如把 7 天改成了 30 天Redis 里旧 TTL 不会自动更新还是按 7 天过期。解决方式是修改短链时主动删除缓存下次访问重新回源。这个动作很容易漏我就在后台管理系统里漏过一次结果改了过期时间后用户访问短链直接 410排查了半天才发现是缓存没删。5. 复习 Day04 的一点个人体会整个项目复习到第四天短链这个项目算是走完了一个从 0 到 1 的闭环。坦白讲短链系统的代码量不算大但它的价值在于把太多后端基础知识浓缩在一个小场景里进制转换让你重新理解了编码缓存穿透和击穿让你第一次真正关心 Redis 底层301 和 302 的区别让你意识到 HTTP 协议不是背概念而是会影响线上行为的硬规则。就连简单的“短链跳转提示页”背后都藏着对用户信任的理解——大家不是不信任短链接而是不想被一个看不见的地址带到未知的地方。如果你也想拿这个项目练手我的建议是别急着抄代码先自己画一画链路图把请求进来之后每一步要做什么写清楚再动手实现。哪怕画得丑也比直接看别人的源码有收获。遇到问题就打开日志一层层追短链系统因为链路短排查起来相对容易特别适合用来建立“先怀疑缓存、再怀疑 DB、最后怀疑代码”的排查直觉。最后再分享一个小技巧给短码生成逻辑加上单元测试但有时间的话尽量用真实浏览器的无痕窗口去验证一遍完整跳转流程。自动化测试能保证逻辑正确但只有真实浏览器访问你才能感受到 302 跳转的丝滑、中间确认页的体验、以及慢网络下缓存未命中的卡顿。这些“体感”层面的东西才是把一个个人项目从“能跑”推向“能用”的关键。