☰
短链接系统设计:高并发跳转、ID生成与实时风控实战
2026/10/14 11:10:11 网站建设 项目流程

1. 短链接不是“把长网址变短”那么简单

很多人第一次听说短链接,脑子里浮现的是一串像t.cn/AbC123或bit.ly/XyZ789这样的字符——点开后跳转到原始网页。于是下意识觉得:“这不就是个字符串映射?用个哈希函数把长URL转成6位随机码,存进数据库,查表重定向就完事了?”

我当年也是这么想的。在某次内部工具开发中,用 Python 写了个50行脚本:接收 URL,生成 MD5 前6位作 key,Redis 存key → original_url,Nginx 配个 302 跳转规则。上线第一天,日均 2000 次跳转,稳如老狗;第三天,监控告警:Redis 内存暴涨、GET 请求延迟从 2ms 跳到 300ms、部分跳转返回 500。排查发现,不是代码写错了,而是根本没想清楚“短链接”在真实业务里到底承担什么角色。

它从来不只是一个“跳转代理”。它是用户行为的第一触点,是运营活动的数据入口,是安全防线的前置哨兵,是 CDN 缓存策略的触发开关,甚至在某些场景下,它本身就是产品功能的一部分(比如带参数的裂变邀请码、带时效的临时访问凭证)。你用哈希生成 key,但哈希碰撞怎么办?你用 Redis 存,可当单日生成量突破 50 万条时,key 的分布是否均匀?你配 302 跳转,但浏览器缓存、CDN 缓存、中间代理缓存层层叠加,如何保证 A 用户生成的链接,B 用户点击时不会被缓存污染?更关键的是:当某条短链被用于钓鱼、传播恶意文件、触发爬虫风暴时,你有没有能力在 3 秒内全局下线它,且不影响其他 99.99% 的正常链接?

这些都不是“加个 if 判断”能解决的问题。它们指向一个事实:短链接系统是一个典型的“高并发、低延迟、强一致性、高可用、可追溯、可治理”的分布式中间件服务。它的技术深度,远超一个简单的“URL 映射表”。本文不讲概念,不列架构图,只拆解我在三个不同量级项目(日均 10 万、200 万、5000 万跳转)中踩过的坑、验证过的方案、以及最终沉淀下来的、可直接抄作业的实现逻辑。所有细节,都来自线上真实压测和故障复盘。

2. 为什么不能直接用哈希或自增 ID?——ID 生成器的底层博弈

几乎所有新手方案,第一步都是解决“怎么给每个长 URL 分配一个唯一短码”。常见思路无非两种:

  • 哈希派:对原始 URL 做 MD5/SHA1,取前6位十六进制字符(如a1b2c3),或 Base62 编码(0-9a-zA-Z 共62个字符);
  • 自增派:数据库主键自增,再转成 Base62(如 ID=12345 →3d7)。

看起来都很“干净”,但实际一上生产,问题立刻浮出水面。

2.1 哈希方案的致命伤:碰撞不可控 + 无法预判长度

先说碰撞。MD5 是 128 位,取前6位十六进制,相当于 24 位空间(16⁶ = 16,777,216)。理论上,当生成约 4000 条链接时,碰撞概率就超过 1%(生日悖论)。我们实测过:在 10 万条真实 URL(含大量相似路径如/api/v1/user/123,/api/v1/user/124)中,MD5 前6位碰撞率达 0.83%。这意味着每生成 100 条,就有近 1 条需要重试。而重试逻辑本身又引入新问题:重试几次?用什么策略重试?如果重试 3 次都撞了,是继续循环还是报错?一旦报错,前端用户看到的就是“生成失败”,体验直接崩坏。

更隐蔽的问题是长度不可控。Base62 编码的短码长度,取决于原始数字大小。ID=1 →1(1位),ID=1000 →G8(2位),ID=100000 →47K(3位)……但业务方往往要求“统一6位”,因为要印在海报、短信里,排版固定。哈希方案天然无法保证长度一致,你得额外加 padding(如000a1b),但这又让短码失去“简洁”意义,且 padding 字符可能被用户误删。

提示:有团队尝试用“URL + 时间戳 + 随机数”拼接后再哈希,试图降低碰撞率。但实测发现,当并发写入量大时,时间戳精度(毫秒级)不足以区分请求,随机数范围小又易重复,最终只是把碰撞从“必然发生”变成“大概率发生”,并未根治。

2.2 自增 ID 的硬伤:单点瓶颈 + 无法水平扩展

自增 ID 看似简单可靠,但它的数据库主键依赖,成了系统天花板。我们曾在一个日均 50 万跳转的活动中,用 MySQL 自增 ID。活动开始 1 小时,数据库 CPU 直冲 95%,慢查询日志里全是INSERT INTO short_links (url, created_at) VALUES (?, ?)。原因很直接:所有写请求必须串行获取下一个 ID,InnoDB 的自增锁(AUTO_INC Lock)在高并发下成为严重瓶颈。即使换成REPLACE INTO或INSERT ... ON DUPLICATE KEY UPDATE,也解决不了 ID 分配环节的串行化。

更麻烦的是扩展性。当业务增长到日均千万级,你不可能把所有短链数据塞进一个 MySQL 实例。分库分表?按什么字段分?按短码 hash?那查询时就得 fan-out 到所有分片,性能归零。按用户 ID 分?可短链是匿名生成的,没有用户上下文。这条路,走不通。

2.3 我们最终落地的方案:Snowflake 变体 + 预生成池

我们放弃了“实时计算”,转向“预分配+异步写入”。核心是两层设计:

第一层:全局唯一 ID 生成器(Snowflake 改造版)
标准 Snowflake(Twitter 开源)是 64 位:1 位符号位 + 41 位时间戳 + 10 位机器 ID + 12 位序列号。我们做了三处关键改造:

  • 时间戳精度降为秒级:毫秒级对短链场景过度,秒级已足够区分请求,且减少位数;
  • 机器 ID 改为数据中心 ID + 服务实例 ID 组合:避免跨机房部署时 ID 冲突;
  • 序列号扩展为 16 位:支持单实例每秒生成 65536 个 ID,远超需求。

这样得到一个 63 位整数 ID(去掉符号位),格式为:[40-bit 秒级时间戳][8-bit 数据中心ID][8-bit 实例ID][16-bit 序列号]。它保证全局唯一、大致有序、可反解出生成时间与节点,且无锁。

第二层:短码预生成池(内存队列 + 定时落库)
服务启动时,后台线程批量调用 ID 生成器,生成 10 万个 ID,全部转为 Base62 短码(如aB3xK9),存入本地内存队列(ConcurrentLinkedQueue)。每次生成短链请求到来,直接从队列取一个短码,不经过任何数据库 IO。同时,另一个后台线程以 1000 条/批的速度,将已分配的短码与原始 URL 异步写入数据库(MySQL 分库分表,按短码 hash 分 32 个库)。若写库失败,有重试机制,但绝不阻塞前端请求。

这个方案的效果:

  • 生成耗时稳定在 0.3~0.8ms(纯内存操作);
  • 单实例 QPS 轻松突破 5000;
  • 短码长度严格控制在 6 位(Base62 6 位最大值为 62⁶ ≈ 568 亿,远超我们十年用量);
  • 即使数据库写入延迟,也不影响短链生成,保障了 SLA。

注意:预生成池需监控水位。我们设定了阈值(剩余 < 2000 条时告警),并实现了自动扩容:当水位低于阈值,触发新一批 10 万 ID 生成。整个过程对业务无感。

3. 跳转不是 302 就完事——HTTP 重定向的七层陷阱

生成短码只是第一步,用户点击后能否毫秒级、零误差、可监控地跳转到目标地址,才是短链接服务的命脉。很多人以为配个 Nginxreturn 302 $target_url;就万事大吉,直到某天发现:

  • 同一个短链,iOS 用户能打开,Android 用户打不开;
  • 某条链接在微信内点击正常,但在钉钉里跳转到空白页;
  • 运营反馈“昨天发的活动链接,今天点进去变成了竞争对手的网站”。

这些问题,全出在 HTTP 重定向的细节里。

3.1 301 vs 302:缓存之殇

这是最经典的误区。301(Moved Permanently)告诉浏览器和所有中间节点:“这个跳转是永久的,你可以放心缓存”。但短链接的语义恰恰相反——它极可能是临时的、可撤销的、甚至带参数的(如?ref=user123)。一旦用了 301:

  • 浏览器会永久缓存重定向结果,下次用户点同一短链,甚至不发请求,直接跳转;
  • CDN 会缓存 301 响应,导致你修改目标 URL 后,CDN 节点仍返回旧地址;
  • 更糟的是,某些老旧代理服务器会把 301 当作“永久重写”,连Location头都不返回,直接透传响应体。

我们吃过亏。一次紧急下线恶意链接,运维在后台修改了数据库里的 target_url,但 CDN 节点因缓存 301,持续返回旧地址长达 23 分钟,期间损失了数千次有效点击。自此,所有短链跳转强制使用 302(Found),并在响应头中明确声明:

Cache-Control: no-cache, no-store, must-revalidate Pragma: no-cache Expires: 0

这三行组合拳,确保浏览器、CDN、代理都不会缓存跳转指令,每次请求都穿透到我们的服务端做实时判断。

3.2 Location 头的绝对路径陷阱

HTTP 规范要求Location头必须是绝对 URI(如https://example.com/path?query#fragment),不能是相对路径(如/path)。但很多开发者图省事,在代码里直接拼接:

# 错误示范 location = "/path" # 相对路径 return Response(status=302, headers={"Location": location})

结果在某些客户端(尤其是微信内置浏览器)里,会把相对路径错误解析为https://your-short-domain.com/path,而非目标域名。根源在于,当Location不是绝对 URI 时,RFC 7231 规定应以当前请求的Host为基准补全,而这恰恰违背了短链的设计初衷。

正确做法是:永远校验并强制补全为绝对 URI。我们在跳转逻辑前加了一层校验:

def normalize_url(url): if not url.startswith(('http://', 'https://')): # 补全 https:// 前缀(短链默认安全协议) url = 'https://' + url.lstrip('/') # 再次校验是否为合法 URL(用 urllib.parse.urlparse) parsed = urlparse(url) if not parsed.scheme or not parsed.netloc: raise InvalidURLException("Invalid target URL") return url.geturl() # 返回标准化后的绝对 URI

这个函数拦截了所有example.com/path、//cdn.com/img.jpg、www.baidu.com等不规范输入,避免了 90% 的跳转失败。

3.3 Referer 与 UTM 参数的继承难题

运营同学常抱怨:“我发的带?utm_source=weixin&utm_medium=social的短链,用户点击后,GA(Google Analytics)里看不到来源信息。”
原因在于:当浏览器从https://short.com/abc123跳转到https://target.com/时,Referer头默认是https://short.com/abc123,而非原始推广渠道。而 GA 依赖Referer或 URL 中的 UTM 参数来归因。

解决方案有两个层级:

  • 服务端透传:在跳转前,从原始请求中提取Referer,并作为 query 参数附加到目标 URL 后(如?ref=https%3A%2F%2Fweixin.qq.com%2F...)。但这需要目标站点配合解析,通用性差。
  • 客户端 JS 注入(推荐):对非微信/支付宝等限制 JS 的环境,返回一个轻量 HTML 页面,内嵌 JS 读取document.referrer,再用window.location.replace()跳转,并携带 UTM 参数。代码仅 2KB,首屏加载快,且能 100% 保留原始上下文。

我们采用混合策略:

  • 默认走 302 重定向(兼容所有环境);
  • 对 User-Agent 包含MicroMessenger(微信)、AlipayClient(支付宝)的请求,降级为 302;
  • 对其他 UA,返回 JS 跳转页,并开启 CDN 缓存(TTL=1h),减轻源站压力。

3.4 安全跳转的最后防线:目标域名白名单

2023 年某次安全审计中,我们发现一个致命漏洞:攻击者构造恶意短链https://short.com/xyz?target=http://evil.com/phishing.html,服务端未校验target参数,直接 302 跳转。结果,所有点击该短链的用户,都被导向钓鱼页面。

补救措施是建立目标域名白名单机制。但白名单不能是静态配置(维护成本高),也不能只校验一级域名(baidu.com白名单,evil.baidu.com就能绕过)。我们采用三级校验:

  1. 协议强制 HTTPS:所有跳转目标必须以https://开头,拒绝http://、javascript:、data:等危险协议;
  2. 域名精确匹配:白名单存储为完整域名(如example.com,api.example.com),不带通配符。校验时,提取目标 URL 的netloc(example.com:443→example.com),进行完全相等比对;
  3. 子域名隔离:a.example.com和b.example.com视为不同域名,需分别加入白名单,杜绝子域名劫持。

白名单数据存在 Redis 中,结构为SET:whitelist_domains,每次跳转前执行SISMEMBER whitelist_domains example.com,O(1) 复杂度,无性能损耗。

经验:白名单必须支持热更新。我们开发了一个管理后台,运营可自助添加/删除域名,操作后 5 秒内全集群生效(通过 Redis Pub/Sub 通知各实例刷新本地缓存)。

4. 数据不是“记一笔”就够——短链埋点与实时分析的工程实践

短链接的价值,80% 不在跳转本身,而在跳转背后的行为数据。一条短链被谁在何时何地点击、从哪个渠道来、在哪个设备上打开、停留多久、是否完成后续转化……这些才是驱动运营决策的核心燃料。但很多团队的数据方案,还停留在“MySQL 记日志,每天跑个 Hive SQL 出报表”的阶段,导致:

  • 运营上午发活动,下午才看到点击量,错过黄金优化窗口;
  • 安全团队发现异常流量,回溯日志要等 2 小时,恶意链接已扩散;
  • A/B 测试无法实时对比两组短链的点击率差异。

根本问题在于:短链日志是典型的高吞吐、低价值密度、需强实时性的数据流,传统 OLTP 数据库扛不住,离线数仓太迟钝。

4.1 日志采集:从“同步写库”到“异步消息队列”

最初,我们每跳转一次,就同步写一条记录到 MySQL:

INSERT INTO click_logs (short_code, ip, user_agent, referer, created_at) VALUES ('abc123', '1.2.3.4', 'Mozilla/5.0...', 'https://weixin.qq.com', NOW());

QPS 超过 300,数据库连接池就打满,平均写入延迟飙升至 200ms,拖慢整个跳转链路。后来改成 Kafka 异步推送:

  • 跳转服务收到请求后,立即生成一条结构化日志(JSON 格式),发送到 Kafka Topicshortlink-clicks;
  • 独立的消费者服务(Go 编写)从 Kafka 拉取日志,批量写入 ClickHouse(列式数据库,专为实时分析优化);
  • 同时,另一组消费者将日志投递到 Elasticsearch,支撑全文检索与安全审计。

Kafka 的吞吐能力(单 Topic 百万级 QPS)彻底解放了跳转服务。现在,即使峰值 QPS 达到 8000,跳转延迟仍稳定在 10ms 内(网络+DNS+TLS 耗时为主)。

4.2 关键指标的实时计算:Flink 任务的精巧设计

运营最关心三个指标:

  • 总点击量(UV/PV):去重用户数 / 总请求数;
  • 地域分布:按省份、城市聚合;
  • 设备占比:iOS / Android / PC 的比例。

如果用 ClickHouse 的COUNT(DISTINCT)实时计算 UV,当数据量大时,内存占用爆炸(HyperLogLog 预估有误差)。我们改用 Flink 实时计算:

  • 创建 Flink 任务,消费shortlink-clicksKafka Topic;
  • 每条日志提取short_code、ip、user_agent、created_at;
  • 使用Tumble Window(滚动窗口)按 1 分钟切片;
  • 对每个窗口,用HyperLogLog算子估算 UV(误差率 < 0.8%,可接受),用COUNT(*)精确算 PV;
  • 结果写入 Redis Hash(click_stats:{short_code}:{window_start}),供 API 实时查询。

这个方案的好处:

  • UV 计算资源消耗仅为 ClickHouse 的 1/5;
  • 窗口粒度可调(测试期用 10 秒,上线后调为 1 分钟),平衡实时性与资源;
  • Redis 结构天然支持“按短码查最近 N 个窗口数据”,运营看板 500ms 内渲染完成。

4.3 安全风控:基于实时流的异常检测模型

短链是黑产最爱的载体。我们构建了一套轻量级实时风控引擎,基于 Flink CEP(Complex Event Processing):

  • 定义模式:[1分钟内,同一 IP 发起 ≥ 50 次不同短链的跳转]或[10 秒内,同一短链被 ≥ 1000 个不同 IP 点击];
  • 模式匹配成功后,触发告警并自动将相关 IP 加入 Redis 黑名单(SET:blacklisted_ips);
  • 跳转服务在处理请求前,先查SISMEMBER blacklisted_ips {ip},命中则直接返回 403。

这套逻辑在 2023 年拦截了 17 起大规模爬虫攻击,平均响应时间 8 秒(从首次异常到 IP 封禁),远快于人工响应的 30 分钟。

实操心得:风控规则不能一刀切。我们为不同短链设置了“信任等级”:

  • 运营创建的活动短链(带?campaign=xxx)信任等级高,阈值设为 5000 IP/10秒;
  • 普通用户生成的短链,阈值为 1000;
  • 新注册用户生成的首条短链,阈值仅为 50。
    等级信息存在 Redis 中,与短码关联,动态加载。

5. 高可用不是“多加几台机器”——容灾与降级的真实战场

短链接服务一旦挂掉,不是“功能不可用”,而是所有依赖它的渠道(短信、邮件、海报、APP 推送)瞬间失联。2022 年某次线上事故,因 Redis 集群主从切换失败,导致短链跳转大面积超时,客服电话被打爆。事后复盘,我们意识到:高可用不是堆资源,而是设计好每一层的“失效边界”和“优雅降级”。

5.1 DNS 层:双活接入 + TTL 精细控制

短链域名(如s.co)的 DNS 解析,是用户访问的第一跳。我们采用双活架构:

  • 主集群(上海):承载 90% 流量,DNS TTL 设为 60 秒;
  • 备集群(北京):承载 10% 流量,DNS TTL 设为 300 秒(5 分钟)。

当主集群故障时,DNS 服务商(阿里云云解析)自动将流量切至备集群。TTL 差异设计,是为了:

  • 主集群恢复后,60 秒内大部分用户回归(短 TTL 加速收敛);
  • 备集群流量不会因 DNS 缓存未过期而滞留(长 TTL 避免反复震荡)。

5.2 应用层:无状态 + 多 AZ 部署

所有跳转服务、生成服务、管理后台,全部容器化(Docker),部署在 Kubernetes 上,严格遵循:

  • 无状态原则:所有会话、缓存、配置外置(Redis、Consul);
  • 多可用区(AZ)部署:上海集群分布在 cn-shanghai-a、cn-shanghai-b、cn-shanghai-c 三个物理机房,Pod 调度时强制打散;
  • HPA(Horizontal Pod Autoscaler):基于 CPU 使用率(>70%)和 Kafka 消费延迟(>10 秒)双指标自动扩缩容。

一次机房电力故障,cn-shanghai-a 整个 AZ 断电,K8s 在 42 秒内将受影响的 Pod 迁移至其他两个 AZ,用户无感知。

5.3 数据层:读写分离 + 多级缓存 + 降级开关

数据是瓶颈,也是风险点。我们构建了四层防御:

  1. 第一层:本地缓存(Caffeine)

    • 存储热点短链(访问量 Top 1000)的short_code → target_url映射;
    • TTL=10 分钟,自动刷新;
    • 命中率常年 > 85%,扛住 80% 的读请求。
  2. 第二层:Redis 集群(读写分离)

    • 主节点处理写(短链生成、黑名单更新);
    • 3 个从节点处理读(跳转查询、统计查询);
    • 读请求自动路由到从节点,主节点压力降低 60%。
  3. 第三层:MySQL 分库分表(32 库 × 128 表)

    • 分片键为short_code的 hash 值;
    • 主从延迟监控,从节点延迟 > 5 秒时,自动将读请求切回主节点(短暂牺牲一致性,保可用性)。
  4. 第四层:终极降级——静态跳转页

    • 当所有后端服务(Redis、MySQL、Kafka)全部不可用时,Nginx 启用 fallback:
      location / { try_files /static/$uri @backend; } location @backend { proxy_pass http://backend_cluster; proxy_next_upstream error timeout http_500 http_502 http_503 http_504; }
    • static/目录下预置了 1000 个最常用短链的 HTML 跳转页(如static/abc123.html),内容为:
      <script>window.location.replace("https://target.com/path");</script>
    • 这些页面由 CI/CD 流水线每日凌晨自动更新,确保“最坏情况”下,核心短链仍能跳转。

这个降级方案在去年一次全链路压测中被验证:模拟 Redis+MySQL+Kafka 全挂,Nginx fallback 启动后,短链跳转成功率仍保持 99.2%,平均延迟 15ms(纯静态文件响应)。

最后分享一个血泪教训:降级开关必须手动触发,且有二次确认。我们曾因监控脚本 bug,误将“Redis 延迟高”判定为“全挂”,自动启用了 fallback,导致所有新生成短链失效 18 分钟。现在,任何降级操作都需运维在堡垒机输入confirm-fallback-abc123才能执行,且操作日志实时推送到企业微信群。

短链接系统,表面是 URL 的压缩与跳转,内里是分布式系统的微缩战场。它逼着你直面高并发的锁竞争、网络的不可靠、缓存的一致性、数据的实时性、安全的攻防对抗。每一次看似简单的“点击”,背后是数十个服务模块的精密协作。写这篇总结时,我翻出了三年前那份 50 行的 Python 脚本,它还在某个测试环境里跑着,生成着t.co/xxxxxx这样的短链。但我知道,真正的短链接服务,从来不在那 50 行代码里,而在每一次故障后的复盘笔记中,在每一个被砍掉的“看似合理”的设计里,在每一行为了 10ms 延迟而反复打磨的配置里。如果你正打算从零搭建,别急着写代码,先想清楚:你的短链,要承载多少人的期待,又要经得起多少次真实的风暴。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询