i007.cc

i007.cc

优先队列-降维打击

05.价值资料

设计 URL 短链接服务(TinyURL)

经典系统设计题,考察存储、编码、扩展性三个核心维度。


第一步:明确需求

功能需求:

  • 输入长 URL,返回短链(如 tinyurl.com/abc123
  • 访问短链,重定向到原始长 URL
  • (可选)自定义短码、设置过期时间

非功能需求(面试重点):

  • 高可用:服务不能挂
  • 低延迟:重定向要快,< 10ms
  • 规模:假设每天 1 亿次写入,100 亿次读取(读多写少,约 100:1)

估算:

写 QPS:100M / 86400 ≈ 1200/s
读 QPS:1200 × 100 = 120,000/s
5年存储:1200/s × 5年 × 3×10^7s ≈ 1800亿条
每条约 500B → 约 90TB

 


第二步:核心问题——如何生成短码?

短码通常 6~8 位,用 [a-zA-Z0-9] 共 62 个字符。6 位能表示 62⁶ ≈ 568 亿个 URL,足够用。

方案一:哈希截断(MD5/SHA256 取前 6 位)
long_url → MD5 → "a3f8b2c91d..." → 取前6位 → "a3f8b2"

 

问题:哈希冲突。不同长链可能产生相同短码,需要冲突检测(查库,碰撞就换取后 6 位),逻辑复杂且有性能损耗。

方案二:自增 ID + 62 进制编码 ⭐ 推荐

数据库维护一个全局自增 ID,把十进制 ID 转成 62 进制字符串作为短码。

cpp
string encode(long long id) {
    const string chars = "0123456789abcdefghijklmnopqrstuvwxyz"
                         "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    string res;
    while (id > 0) {
        res += chars[id % 62];
        id /= 62;
    }
    reverse(res.begin(), res.end());
    return res;  // ID=1 → "1", ID=125 → "1Z", ...
}

long long decode(const string& code) {
    const string chars = "0123456789abc...";
    long long id = 0;
    for (char c : code) {
        id = id * 62 + chars.find(c);
    }
    return id;
}

 

优点: 天然无冲突,短码有序,实现简单。
问题: 单点瓶颈——全局 ID 生成器是单点,需要进一步设计。


第三步:系统架构

用户
 │
 ├─ POST /shorten ──→ [写服务] ──→ [ID生成器] ──→ [MySQL/主库]
 │                                               ──→ [Cache写入]
 │
 └─ GET /{code}  ──→ [读服务] ──→ [Redis Cache]
                                  │ Cache Miss
                                  └──→ [MySQL/从库] ──→ 回写Cache

 

数据表设计(极简):

sql
CREATE TABLE url_mapping (
    id       BIGINT PRIMARY KEY AUTO_INCREMENT,
    code     VARCHAR(8) UNIQUE,
    long_url VARCHAR(2048) NOT NULL,
    created_at DATETIME,
    expires_at DATETIME
);

 

重定向方式:

  • 301 Permanent Redirect:浏览器缓存,服务器压力小,但无法统计点击
  • 302 Temporary Redirect:每次都走服务器,可以统计点击数 ← 一般选这个

第四步:解决全局 ID 单点问题

方案一:号段模式(Segment)

ID 生成器一次批量申请一段 ID(如 10000 个),放到内存里慢慢用,用完再申请。数据库压力降低 10000 倍。

ID服务: 向DB申请 [1, 10001),缓存在内存
       每次写请求直接从内存取,极快
       用完后再申请 [10001, 20001)

 

方案二:多机 ID 生成(步长错开)

部署 3 台 ID 生成器,步长设为 3,起点分别为 1、2、3:

机器1: 1, 4, 7, 10, ...
机器2: 2, 5, 8, 11, ...
机器3: 3, 6, 9, 12, ...

 

无冲突,无需协调,天然分布式。

方案三:Snowflake

Twitter 的分布式 ID 方案,64 位 = 时间戳(41) + 机器ID(10) + 序列号(12),每毫秒可生成 4096 个不重复 ID,工业界主流选择。


第五步:缓存与扩展

缓存策略(读服务):

读写比 100:1,读服务加 Redis 缓存是最大的性能杠杆。

Key:   short_code
Value: long_url
TTL:   与 URL 过期时间对齐(或固定 24h)
淘汰:  LRU(热点 URL 永远在缓存里)

 

估算:20% 的热点 URL 贡献 80% 流量(二八定律),只缓存热点就够用。

水平扩展:

写服务: 无状态,加机器即可
读服务: 无状态,加机器即可
数据库: MySQL 主从(主写从读)+ 按 code 哈希分库分表
缓存:   Redis Cluster

 


面试时的表达框架

1. 需求确认(2min)  → 功能 + 规模估算
2. 核心设计(5min)  → 短码生成方案选型,讲清楚62进制编码
3. 架构图(5min)    → 读写分离、缓存层、ID生成器
4. 深挖细节(8min)  → 单点问题怎么解、缓存怎么设计、301 vs 302
5. 扩展话题(5min)  → 自定义短码、过期清理、点击统计、防滥用

 

面试官最想听的是你在每个决策点为什么选这个而不是那个,权衡过程比答案本身更重要。

发表回复