2014年,Bitly每月处理60亿次点击和6亿次链接缩短,仅用约30台前端服务器支撑全球流量。这个看似简单的"长变短"功能,却是分布式系统设计的经典考题——它完美浓缩了ID生成、存储选型、缓存策略、高可用设计等核心问题。
从一个朴素的想法说起
2002年,Kevin Gilbertson创建了TinyURL,初衷很简单:让论坛上的长链接更容易分享。当时没人预料到,这个工具会在Twitter时代迎来爆发——140字符的限制让每个字符都变得珍贵。Twitter最初自动将长URL转换为TinyURL,后来转向Bitly,最终在2011年开发了自己的短链接服务t.co。
短链接的核心原理可以用一行伪代码概括:
short_url = domain + "/" + generate_short_code(long_url)
真正的挑战在于generate_short_code的实现,以及支撑这个简单映射背后的系统架构。
短码生成:三种主流方案的博弈
短码生成是整个系统的核心。三种主流方案各有优劣,选择取决于具体场景。
哈希截断 + Base62编码
这是最直观的方案:对长URL进行哈希,截取前几位,编码成URL安全字符。
import hashlib
def generate_short_code(url):
hash_value = hashlib.md5(url.encode()).hexdigest()
# 取前48位(6字节)
truncated = hash_value[:12]
# 转换为整数
decimal_value = int(truncated, 16)
# Base62编码
return base62_encode(decimal_value)
Base62编码使用62个字符:a-z、A-Z、0-9。相比Base64,它排除了+和/这两个在URL中有特殊含义的字符,无需额外转义。
数学上看,n位Base62编码能表示62^n个不同值:
- 6位:62^6 ≈ 568亿
- 7位:62^7 ≈ 3.5万亿
- 8位:62^8 ≈ 218万亿
根据生日悖论,当生成的短码数量达到2^(n/2)时,碰撞概率约为50%。对于48位(6字节)的截断哈希,这个临界点约为2^24 ≈ 1677万。这意味着当你生成了约1600万个短链接时,碰撞概率已经相当可观。
碰撞处理是这套方案的关键。常见策略包括:
- 加盐重哈希:检测到碰撞后,在原URL后追加随机盐值,重新计算
- 后缀递增:在冲突的短码后添加数字后缀,如
abc123→abc123-1
这两种策略都意味着每次生成都需要数据库查询来验证唯一性,增加了写路径的延迟。
全局计数器
这是最可靠的方案:维护一个全局递增计数器,每次生成短链接时获取下一个ID,然后Base62编码。
# Redis实现
def generate_short_code(url):
# 原子递增
unique_id = redis.incr("url_counter")
return base62_encode(unique_id)
Redis的INCR命令是原子的,即使在高并发下也能保证不重复。计数器从1开始,生成的短码序列如下:
- 1,000 →
g8 - 1,000,000 →
4c9B - 1,000,000,000 →
15ftgG
即使生成10亿个短链接,Base62编码后也只需6-7个字符。
单点瓶颈是这套方案的明显缺陷。当写入QPS达到数万时,单个Redis实例可能成为瓶颈。解决方案是分片计数器:将ID空间划分为多个区间,每个分片负责一个区间。
分片1:ID 1 - 1,000,000
分片2:ID 1,000,001 - 2,000,000
...
全局ID的计算公式为:global_id = (shard_id << offset) + local_counter
Snowflake ID:分布式系统的标准答案
Twitter在2010年开源了Snowflake算法,它成为了分布式ID生成的事实标准。一个64位Snowflake ID的组成如下:
| 组成部分 | 位数 | 说明 |
|---|---|---|
| 符号位 | 1位 | 始终为0,保证ID为正数 |
| 时间戳 | 41位 | 毫秒级时间戳,约69年 |
| 机器ID | 10位 | 最多1024台机器 |
| 序列号 | 12位 | 每毫秒最多4096个ID |
packet-beta
title Snowflake ID 64位结构
0 : "0"
1-41 : "时间戳(41位)"
42-51 : "机器ID(10位)"
52-63 : "序列号(12位)"
这个结构带来几个重要特性:
时间有序性:由于时间戳占据高位,ID本身就按创建时间排序。这对于数据库索引、日志分析都极其有利。
分布式友好:每台机器独立生成ID,无需网络协调。理论吞吐量:每台机器每毫秒4096个ID,即单机每秒约400万个ID。
时钟依赖:这是最大的风险。如果机器时钟回拨,可能生成重复ID。生产环境通常配置NTP同步,并设置时钟回拨保护——检测到回拨时暂停ID生成,等待时钟追上。
Discord、Instagram、Mastodon都采用了Snowflake或其变体。Instagram的变体将机器ID扩展为13位,序列号缩减为10位,以适应其数据库分片架构。
存储架构:NoSQL的理由
短链接服务的访问模式非常特殊:100:1的读写比。每创建一个短链接,可能被访问上百次。这意味着存储选型应该优先考虑读取性能,而非事务能力。
关系数据库当然可以工作,但在百亿级数据规模下,NoSQL方案更具优势:
DynamoDB:AWS托管的键值存储,自动分片,内置多区域复制。适合不需要复杂查询的场景。
Cassandra:线性可扩展,无单点故障。写入性能优异,但读取延迟略高。
Redis:可作为缓存层,也可作为主存储(配合持久化)。对于URL映射这种简单的键值数据,Redis的内存存储提供了极致的读取性能。
一个合理的混合架构是:
- 热数据:Redis缓存,命中率90%+
- 温数据:DynamoDB/Cassandra
- 冷数据:S3对象存储,配合生命周期策略
sequenceDiagram
participant Client as 客户端
participant LB as 负载均衡
participant Service as 重定向服务
participant Redis as Redis缓存
participant DB as 数据库
Client->>LB: GET /abc123
LB->>Service: 路由请求
Service->>Redis: GET abc123
alt 缓存命中
Redis-->>Service: 返回长URL
else 缓存未命中
Service->>DB: SELECT long_url WHERE short_code=?
DB-->>Service: 返回长URL
Service->>Redis: SET abc123 long_url
end
Service-->>Client: 302 Redirect
301还是302:一个影响深远的决定
重定向状态码的选择看似小问题,实则影响分析能力、性能和SEO。
301 Moved Permanently告诉浏览器"这个短链接永远不会变"。浏览器和搜索引擎会缓存这个重定向,后续请求直接跳转,不再访问短链接服务。
优点:
- 最快的用户体验(浏览器缓存)
- 减少服务器负载
- 传递SEO权重到目标页面
缺点:
- 失去所有后续点击的分析数据
- 无法修改目标URL
- 无法设置链接过期
302 Found / 307 Temporary Redirect告诉浏览器"这个短链接可能变化"。每次点击都会访问短链接服务。
优点:
- 完整的点击追踪
- 可随时修改目标URL
- 支持链接过期
缺点:
- 略高的延迟
- 更大的服务器负载
Bitly和大多数商业短链接服务默认使用302。原因很简单:分析数据是它们的核心价值。但对于纯粹追求性能的场景(如营销活动落地页),301可能是更好的选择。
实际上,聪明的方案是混合使用:
- 新创建的链接使用302,收集初期数据
- 分析流量稳定后,可以选择切换到301
高可用设计:从单机房到全球部署
Bitly在2014年的架构揭示了高可用的关键原则:服务隔离,独立故障。
他们使用SOA架构,将系统拆分为数十个独立服务:
- URL缩短服务(同步,需快速响应)
- 分析服务(异步,可延迟处理)
- 存档服务(异步,写入HDFS/S3)
当分析服务故障时,URL缩短服务继续正常工作——只是暂时没有分析数据。这种隔离避免了级联故障。
异步消息队列是实现隔离的关键技术。Bitly使用自研的NSQ(现已开源)。当短链接被点击时,重定向服务同步返回响应,同时将点击事件推入队列。下游的分析、存档、实时统计服务各自消费事件,互不影响。
事件优于命令是Bitly的重要经验。命令式消息说"执行X",暗示了接收者的存在。事件式消息说"X发生了",发布者不需要知道谁在消费。这种设计让新增消费者变得极其简单——只需订阅事件流。
flowchart LR
subgraph 同步路径
A[客户端请求] --> B[重定向服务]
B --> C[Redis缓存]
B --> D[(数据库)]
B --> E[302响应]
end
subgraph 异步路径
B --> F[消息队列]
F --> G[分析服务]
F --> H[存档服务]
F --> I[实时统计]
end
多区域部署是保障高可用的最终手段。DNS层面的地理路由可以将用户引导到最近的数据中心。数据同步策略取决于一致性要求:
- 最终一致:跨区域异步复制,延迟低但可能丢失最近数据
- 强一致:跨区域同步复制,延迟高但数据完整
对于短链接服务,最终一致通常是可接受的——一个刚创建的链接在几秒内可能无法从另一个区域访问,但这比完全不可用要好得多。
URL规范化:避免重复的艺术
同一个网页可能有多个URL表示:
http://example.comhttp://example.com/https://example.com/https://www.example.com/
如果不对URL进行规范化,同一个目标可能占用多个短链接。规范化步骤包括:
- 协议标准化:统一使用小写
http或https - 域名标准化:移除或添加
www前缀 - 路径标准化:移除尾部斜杠或统一添加
- 端口标准化:移除默认端口(
:80、:443) - 查询参数排序:将参数按字母顺序排列
更进一步的优化是查询参数清理:移除追踪参数如utm_source、fbclid,这些参数影响URL唯一性,但不影响目标内容。
容量规划:数字背后的含义
假设一个中等规模短链接服务的预期:
- 每日新增:1000万条
- 每日点击:10亿次
- 数据保留:5年
存储计算: 每条记录约300字节(短码+长URL+元数据),5年总数据量:
- 1000万 × 365 × 5 × 300字节 ≈ 5.5TB
QPS计算:
- 写入QPS:1000万 ÷ 86400 ≈ 120
- 读取QPS:10亿 ÷ 86400 ≈ 11,600
这个读写比(约100:1)决定了系统应该针对读取优化。如果缓存命中率达到95%,数据库只需承担580 QPS——任何主流数据库都能轻松应对。
短码长度选择: 5年累积记录数:1000万 × 365 × 5 ≈ 182.5亿
7位Base62编码支持约3.5万亿唯一值,是预期数据量的近200倍,提供充足的安全边际。6位编码支持约568亿,虽然够用,但安全边际较小。
安全考量:便捷与风险的平衡
短链接的匿名性是一把双刃剑。它让分享更便捷,但也成为网络钓鱼和恶意软件传播的工具。
滥用防护措施:
- 黑名单检查:使用Google Safe Browsing等API检查目标URL
- 速率限制:限制单个IP或用户的创建频率
- 人工审核:对高风险链接进行人工审核
- 链接过期:为链接设置默认过期时间
链接预览是保护用户的重要功能。用户可以通过添加preview.前缀或特殊页面查看目标URL,如https://preview.tinyurl.com/abc123会显示目标地址,让用户确认后再跳转。
Wikipedia明确禁止在文章中使用短链接,Reddit社区强烈建议不要使用——这些都是对短链接匿名性风险的直接回应。
尾声:简单背后的复杂
一个看似"用哈希表就能实现"的功能,在规模化后展现出惊人的复杂性。从Base62编码的数学优雅,到Snowflake ID的分布式智慧;从302重定向的分析权衡,到多区域部署的容灾考量——短链接服务是分布式系统设计的绝佳案例。
Bitly用约30台前端服务器支撑百亿级月点击量,背后的秘诀是:正确的架构选择比更多的硬件更有效。读写分离、异步解耦、分层缓存——这些原则不仅适用于短链接服务,也是所有高并发系统的通用智慧。
当你下次点击一个短链接时,也许会想起这个简单的重定向背后,有着怎样的技术积淀。
参考资料
- Karger, D., et al. “Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web.” ACM STOC, 1997.
- Twitter Engineering. “Announcing Snowflake.” Twitter Blog, June 2010.
- O’Connor, S. “Lessons Learned Building Distributed Systems at Bitly.” Bacon Conference, 2014.
- Wikipedia. “URL Shortening.” https://en.wikipedia.org/wiki/URL_shortening
- Wikipedia. “Snowflake ID.” https://en.wikipedia.org/wiki/Snowflake_ID
- AlgoMaster. “Design URL Shortener.” https://algomaster.io/learn/system-design-interviews/design-url-shortener
- High Scalability. “Bitly: Lessons Learned Building a Distributed System that Handles 6 Billion Clicks a Month.” 2014.
- Coding Horror. “URL Shortening: Hashes In Practice.” https://blog.codinghorror.com/url-shortening-hashes-in-practice/
- System Design Handbook. “Design a Unique ID Generator in Distributed Systems.” 2025.
- Google. “Canonicalization.” https://developers.google.com/search/docs/crawling-indexing/canonicalization