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-zA-Z0-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万个短链接时,碰撞概率已经相当可观。

碰撞处理是这套方案的关键。常见策略包括:

  1. 加盐重哈希:检测到碰撞后,在原URL后追加随机盐值,重新计算
  2. 后缀递增:在冲突的短码后添加数字后缀,如abc123abc123-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.com
  • http://example.com/
  • https://example.com/
  • https://www.example.com/

如果不对URL进行规范化,同一个目标可能占用多个短链接。规范化步骤包括:

  1. 协议标准化:统一使用小写httphttps
  2. 域名标准化:移除或添加www前缀
  3. 路径标准化:移除尾部斜杠或统一添加
  4. 端口标准化:移除默认端口(:80:443
  5. 查询参数排序:将参数按字母顺序排列

更进一步的优化是查询参数清理:移除追踪参数如utm_sourcefbclid,这些参数影响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亿,虽然够用,但安全边际较小。

安全考量:便捷与风险的平衡

短链接的匿名性是一把双刃剑。它让分享更便捷,但也成为网络钓鱼和恶意软件传播的工具。

滥用防护措施

  1. 黑名单检查:使用Google Safe Browsing等API检查目标URL
  2. 速率限制:限制单个IP或用户的创建频率
  3. 人工审核:对高风险链接进行人工审核
  4. 链接过期:为链接设置默认过期时间

链接预览是保护用户的重要功能。用户可以通过添加preview.前缀或特殊页面查看目标URL,如https://preview.tinyurl.com/abc123会显示目标地址,让用户确认后再跳转。

Wikipedia明确禁止在文章中使用短链接,Reddit社区强烈建议不要使用——这些都是对短链接匿名性风险的直接回应。

尾声:简单背后的复杂

一个看似"用哈希表就能实现"的功能,在规模化后展现出惊人的复杂性。从Base62编码的数学优雅,到Snowflake ID的分布式智慧;从302重定向的分析权衡,到多区域部署的容灾考量——短链接服务是分布式系统设计的绝佳案例。

Bitly用约30台前端服务器支撑百亿级月点击量,背后的秘诀是:正确的架构选择比更多的硬件更有效。读写分离、异步解耦、分层缓存——这些原则不仅适用于短链接服务,也是所有高并发系统的通用智慧。

当你下次点击一个短链接时,也许会想起这个简单的重定向背后,有着怎样的技术积淀。


参考资料

  1. Karger, D., et al. “Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web.” ACM STOC, 1997.
  2. Twitter Engineering. “Announcing Snowflake.” Twitter Blog, June 2010.
  3. O’Connor, S. “Lessons Learned Building Distributed Systems at Bitly.” Bacon Conference, 2014.
  4. Wikipedia. “URL Shortening.” https://en.wikipedia.org/wiki/URL_shortening
  5. Wikipedia. “Snowflake ID.” https://en.wikipedia.org/wiki/Snowflake_ID
  6. AlgoMaster. “Design URL Shortener.” https://algomaster.io/learn/system-design-interviews/design-url-shortener
  7. High Scalability. “Bitly: Lessons Learned Building a Distributed System that Handles 6 Billion Clicks a Month.” 2014.
  8. Coding Horror. “URL Shortening: Hashes In Practice.” https://blog.codinghorror.com/url-shortening-hashes-in-practice/
  9. System Design Handbook. “Design a Unique ID Generator in Distributed Systems.” 2025.
  10. Google. “Canonicalization.” https://developers.google.com/search/docs/crawling-indexing/canonicalization