# 短链接服务 架构模板 > **代表产品**:Bitly、TinyURL、t.co(Twitter)、各类二维码 / 分享短链 > **一句话定位**:把一条长 URL 映射成一个极短的 key,点击短链时以最快速度重定向回原始地址。 --- ## 1. 一句话定位 短链接服务 = **一张巨大的「短码 → 长链接」映射表** + **一条被优化到极致的重定向快路**。 它看起来简单到像一行哈希表,但恰恰是**「读多写少、极致可用、全局唯一 ID」的教科书**。真相是:99.9% 的流量都是「拿短码查长链,然后 302 跳走」这一个动作——这条路必须**快到几十毫秒、且几乎不能挂**(链接一旦印在海报、二维码、短信里,挂了就是大面积事故)。 ## 2. 业务本质:它在解决什么问题 长链接又长又丑:短信有字数限制、二维码越长越密难扫、社交分享要好看可点。短链解决的是「**把任意长的地址压缩成一个干净、可分发、可追踪的入口**」。 附带的真正价值是**点击分析**:谁、什么时候、在哪、用什么设备点了这条链接。 钱从哪来:企业版的点击分析与营销报表、自定义品牌域名、批量生成 API、A/B 投放追踪。**短链的本体几乎不赚钱,赚钱的是它背后那层「可度量的分发」。** ## 3. 核心需求与约束 **功能性需求:** - [ ] 长链接 → 生成短码 - [ ] 短码 → 重定向回长链接(核心中的核心) - [ ] 自定义别名(`/spring-sale`)、过期时间 - [ ] 点击统计(次数、来源、地域、设备) **非功能性需求 / 质量属性(这才是架构主战场):** | 质量属性 | 目标 | 为什么对这类系统重要 | |---|---|---| | **重定向延迟** | < 50ms | 用户点了要立刻跳,慢一点就像「这链接坏了」 | | **可用性** | 99.99%+ | 链接已经发出去、印出去了,服务挂 = 所有人点了都打不开 | | **读写比** | 常 100:1 甚至 1000:1 | 创建一次,被点千万次。读写形态严重不对称 | | **短码长度** | 尽量短 | 越短越好用,但越短可用空间越小,是个取舍 | **关键约束(不可逾越的边界):** - 🔴 **链接不可变**:短码一旦生成并分发,就印在物料上,永远改不了。 - 🔴 **短码空间有限**:可用短码数 = 字符集大小 ^ 长度。6 位 Base62 ≈ 568 亿,看着多,热门服务几年就逼近。 - 🔴 **读写极度不对称**:架构必须围绕「读」来设计,「写」可以慢。 ## 4. 架构全景图 ``` 创建短链(少量) 点击短链(海量,99.9% 流量) │ │ ▼ ▼ ┌────────────────┐ ┌────────────────────────────┐ │ 写服务 │ │ 读服务 / 重定向(快路) │ │ • 校验长链 │ │ ┌──────────────────────┐ │ │ • 申请唯一 ID │◀── 发号器 ──┐ │ │ 1. 查 KV 缓存(命中即返)│ │ │ • ID → Base62 │ (号段/雪花) │ │ └──────────┬───────────┘ │ │ • 落库 │ │ │ │ 未命中 │ └───────┬────────┘ │ │ ▼ │ │ │ │ ┌──────────────────────┐ │ ▼ │ │ │ 2. 查主存储,回填缓存 │ │ ┌────────────────┐ │ │ └──────────┬───────────┘ │ │ 主存储(映射表) │◀───────────┘ │ ▼ │ │ short_key→long │◀───────────────────│ 3. 返回 301/302 重定向 │ └────────────────┘ └─────────────┬──────────────┘ │ 异步上报 ▼ ┌──────────────────────┐ │ 点击事件队列 → 分析存储 │ (绝不拖慢重定向) └──────────────────────┘ ``` > 灵魂部件是中间那条**重定向快路**:它要承受全部读流量,所以「缓存命中率」几乎等于这个系统的命运。其余一切都是为它服务。 ## 5. 组件职责 - **写服务**:校验目标 URL、申请全局唯一 ID、把 ID 编码成短码、落库。*为什么需要*:创建是低频但要保证短码全局不重复。 - **发号器 / ID 生成**:产出全局唯一、不冲突的 ID。*为什么需要*:短码唯一性的根基;它一旦成为单点或瓶颈,整个写入就卡死(见决策 1)。 - **读服务 / 重定向**:查缓存 → 查库 → 返回重定向。*为什么需要*:这是系统 99.9% 的工作,必须独立、极简、可大量水平扩展。 - **KV 缓存**:把热门短码的映射放在内存级缓存里。*为什么需要*:热点链接极热(一条爆款可能占全站一半点击),缓存命中率直接决定延迟和成本。 - **主存储(映射表)**:`short_key → long_url` 持久化的真相源。 - **点击事件管道(异步)**:把每次点击作为事件丢进队列,后台慢慢聚合。*为什么需要*:统计绝不能挡在重定向的关键路径上(见决策 3)。 ## 6. 关键数据流 **场景一:创建一条短链(低频写)** ``` 1. 用户提交长 URL ──▶ 写服务:校验合法性 / 黑名单 2. ──▶ 发号器:拿到一个全局唯一 ID(如 10000001) 3. 写服务:把 ID 用 Base62 编码 → "aUKYr";落库 {aUKYr → https://...} 4. 返回 https://sho.rt/aUKYr ``` **场景二:点击短链并重定向(海量读,系统的命脉)** ``` 1. 用户点 https://sho.rt/aUKYr ──▶ 读服务 2. 查 KV 缓存: 命中 ──▶ 直接拿到长链(99% 走这里) 未命中 ──▶ 查主存储,拿到后回填缓存 3. 返回 HTTP 302 + Location: 原始长链 ── 浏览器自动跳转 4. (旁路)把这次点击作为事件异步丢进队列,主流程立刻结束 ◀── 不等统计写完 ``` > 注意第 4 步:**统计是「旁路」,不是「主路」。** 重定向在第 3 步就结束了。这是「把非关键路径从关键路径上剥离」的典型。 ## 7. 数据模型与存储选择 核心实体极简:`映射(short_key, long_url, 创建者, 过期时间)`;`点击事件(short_key, 时间, 来源, 地域, 设备)`。 | 数据 | 存储类型 | 为什么 | |---|---|---| | 短码 → 长链映射 | KV / 关系型(按 short_key 分片) | 主键精确查找、海量、无复杂关联 | | 热点映射 | 内存级 KV 缓存 | 读路径的命脉,命中率决定一切 | | 点击事件 | 时序 / 列存 | 海量追加、按时间和维度聚合,做报表 | > 教学点:这是**主键点查**场景(永远是「拿一个 key 取一个 value」),没有 `JOIN`、没有范围扫描——所以 KV 形态的存储天生最合适,关系型也行但要把它当 KV 用。 ## 8. 关键架构决策与权衡 ⭐ **决策 1:短码怎么生成?** - 自增 ID → Base62:短、无冲突、保证唯一。但**短码连续可枚举**(别人能顺着遍历你全站链接、甚至估出你的业务量)。 - 随机生成:不可枚举、安全。但要**查重**(可能撞码),空间用满后撞码率升高。 - 哈希长链取前几位:相同长链得相同短码(天然去重),但**哈希冲突**要处理。 - **取向**:多数选「自增 ID + 打乱/加盐再 Base62」——既拿到唯一性,又避免明文连续可枚举。代价是发号器要可靠。 **决策 2:重定向用 301 还是 302?(经典取舍)⭐** - **301 永久重定向**:浏览器和 CDN 会**缓存**,后续点击根本不到你服务器——极快、极省。但**你从此收不到点击统计了**(请求被缓存拦截)。而且短码若想换目标就失效。 - **302 临时重定向**:每次都回到你服务器,**点击统计完整**,可灵活改目标。但服务器要扛全部流量。 - **取向**:**要分析数据就用 302**(这是商业模式所在),代价是自己扛流量、靠缓存优化。这一对取舍精准体现了「**可缓存性 vs 可观测性**」的永恒矛盾。 **决策 3:点击统计同步还是异步?** - 同步:重定向时顺便把统计写库。简单,但**把慢的写操作塞进了最该快的路径**,统计存储一抖动,重定向就变慢。 - 异步:点击只往队列里扔一个事件,主流程立刻返回重定向。 - **取向**:必须异步。**重定向是关键路径,统计是旁路,两者绝不能耦死。** ## 9. 规模化与瓶颈 - **第一个瓶颈:读流量打爆。** → 破解:多级缓存(本地 + 分布式 KV)、把热门短链推到 CDN / 边缘节点直接重定向、读服务无状态水平扩。 - **第二个瓶颈:中心发号器成为写入单点。** → 破解:号段模式(一次批发一段 ID 给各节点本地用)或雪花算法(各节点自己生成不冲突 ID)。详见 [电商平台模板](../ecommerce-platform/README.md) 里的发号思路。 - **第三个瓶颈:点击事件洪峰**(爆款链接被疯转)。→ 破解:消息队列削峰(见 [04 · 消息队列/异步](../../tutorial/04-十大核心架构模式.md)),后台批量聚合。 - **第四个瓶颈:映射表太大。** → 破解:按 short_key 哈希分片。因为是纯点查,分片极其干净(没有跨片查询)。 ## 10. 安全与合规要点 - 🔴 **开放重定向 = 天然钓鱼工具**:短链隐藏了真实目标,攻击者爱用它把人骗到恶意站点。架构上必须**校验 / 扫描目标 URL、维护恶意域名黑名单、对可疑链接加风险提示页**。 - **枚举遍历**:连续短码会被爬虫顺序抓取,泄露全部链接和业务量。→ 短码加盐打乱或随机化。 - **滥用刷量**:有人刷点击伪造数据 → 限流、去重、风控。 - **隐私**:点击数据含 IP / 地域 / 设备,属个人信息,要合规处理与脱敏。 ## 11. 常见误区 / 反模式 - ❌ **同步写点击统计,拖慢重定向** → ✅ 统计走异步队列,重定向是神圣的快路。 - ❌ **用明文连续自增做短码** → ✅ 加盐打乱,避免可枚举、可猜量。 - ❌ **不做缓存,每次点击都打主存储** → ✅ 多级缓存,命中率是命根子。 - ❌ **图省事用 301,结果丢了所有分析数据** → ✅ 想清楚要不要统计,再选 301/302。 - ❌ **不校验目标 URL,沦为钓鱼帮凶** → ✅ 黑名单 + 扫描 + 风险拦截。 - ❌ **把它当复杂系统过度设计** → ✅ 它的难点只在「读路径的快和稳」,别的都该极简。 ## 12. 演进路线:MVP → 成长期 → 成熟期 | 阶段 | 规模量级 | 架构长什么样 | 此时该操心什么 | |---|---|---|---| | **MVP** | 日点击 < 10 万 | 单体 + 一个数据库 + 自增 ID 转 Base62;同步统计也无妨 | 先把「生成 + 重定向」跑通,别想太多 | | **成长期** | 百万~千万 | 读写服务拆开;加 KV 缓存;点击统计异步化;发号器换号段模式 | 找读路径瓶颈、把缓存命中率做上去 | | **成熟期** | 亿级 + 全球 | 多区域多活、边缘 / CDN 重定向、映射表分片、独立的实时分析管道 | 全球延迟、容灾、防滥用、分析体系 | ## 13. 可复用要点 - 💡 **读多写少的系统,要把读路径优化到极致,并和写路径彻底解耦。** 这是缓存、读写分离、CQRS 一切的出发点。 - 💡 **301 vs 302 是「可缓存性 vs 可观测性」的微缩模型。** 任何「让中间层帮你缓存」的设计,几乎都要拿「我还看不看得见这次访问」来换。 - 💡 **全局唯一 ID 是分布式系统的基本功**:中心发号、号段、雪花,各有取舍,值得吃透。 - 💡 **把非关键路径(统计)从关键路径(重定向)上剥离**,是降低延迟、提升稳定性的通用手法。 ## 🎯 随堂检验 --- ## 参考原型与延伸阅读 > 本模板基于以下**真实开源项目**整理(短链服务 + 分布式发号)。 **🔧 开源原型(可直接读代码):** - [YOURLS/YOURLS](https://github.com/YOURLS/YOURLS) — 经典自托管短链,短码生成 / 自定义关键词 / 点击统计 / 重定向。 - [shlinkio/shlink](https://github.com/shlinkio/shlink) — 自托管短链服务,读多写少的重定向 + REST API + 多域名短链。 - [thedevs-network/kutt](https://github.com/thedevs-network/kutt) — 现代化短链(Node.js),链接管理 / 统计 / 自定义域名。 - [twitter-archive/snowflake](https://github.com/twitter-archive/snowflake) — 分布式时序唯一 ID 发号(时间戳+机器ID+序列号),短码发号的经典方案。 --- > 📌 一句话记住短链接服务:**它不是「一行哈希表」,而是「一条要扛住全世界点击、且永远不能挂的重定向快路」——所有设计都在回答『怎么让查 key 这件事又快又稳又省』。**