HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 系统设计实战

    • 系统设计面试教程
    • 系统设计方法论
    • 01-短链系统设计
    • 02 - 秒杀系统设计
    • 03 - IM 即时通讯系统设计
    • 04 - Feed 流系统设计
    • 05 - 分布式 ID 生成器设计
    • 06 - 限流系统设计
    • 第7章:搜索引擎设计
    • 08 - 推荐系统设计
    • 09 - 支付系统设计
    • 10 - 电商系统设计
    • 11 - 直播系统设计
    • 第12章:缓存系统设计
    • 第13章:消息队列设计
    • 第14章:分布式事务
    • 15 - 监控系统设计

01-短链系统设计

面试频率: | 难度: | 推荐准备时间:2-3小时

📖 案例概述

短链系统(URL Shortener)是系统设计面试中最高频的题目之一,因为它:

  • 功能简单易懂,容易在45分钟内完成
  • 涵盖多个技术点(算法、缓存、数据库、高并发)
  • 有明确的优化方向(从简单到复杂)
  • 大厂都有类似系统(bit.ly、tinyurl.com)

真实案例:

  • bit.ly - 每月处理10亿次点击
  • 新浪短链 - t.cn
  • 淘宝短链 - tb.cn
  • 抖音短链 - v.douyin.com

一、需求澄清(5分钟)

1️⃣ 功能需求

面试官:"设计一个短链系统。"

你(需要主动澄清):

Q1: "短链系统的核心功能是什么?"
A: 1. 生成短链(给定长URL,返回短URL)
   2. 短链跳转(访问短URL,重定向到长URL)

Q2: "是否需要自定义短链?比如用户指定 abc123 作为短链码?"
A: 可以支持,但不是核心功能(Nice to Have)

Q3: "是否需要统计功能?比如点击量、访问来源?"
A: 需要基础统计(点击量),高级统计可选

Q4: "短链是否有有效期?"
A: 支持设置有效期,过期后失效

Q5: "是否需要防刷?"
A: 需要考虑,防止恶意刷流量

需求总结:

核心功能(Must Have):

  1. 生成短链:输入长URL,返回短URL
  2. 短链跳转:访问短URL,302重定向到长URL
  3. 基础统计:记录点击量

扩展功能(Nice to Have): 4. 自定义短链:用户指定短链码 5. 有效期管理:支持设置过期时间 6. 详细统计:访问来源、地域、设备等 7. 二维码生成:生成短链的二维码 8. 批量生成:批量转换长URL

2️⃣ 非功能需求

性能要求:
- 读QPS(跳转):10,000(峰值)
- 写QPS(生成):1,000(峰值)
- 响应时间:P99 < 100ms
- 读写比例:10:1(读多写少)

可用性要求:
- 可用性:99.9%(允许每月43分钟故障)
- 数据持久性:不能丢失短链映射关系

一致性要求:
- 短链生成:强一致性(不能重复)
- 统计数据:最终一致性(允许延迟)

扩展性要求:
- 支持水平扩展
- 支持多机房部署

3️⃣ 数据规模

用户规模:
- DAU(日活用户):10,000,000(1000万)
- 每用户每天点击短链:10次
- 每用户每天生成短链:1次

数据量估算:
- 每天生成短链:1000万条
- 每天跳转请求:1亿次
- 数据保留:1年
- 总数据量:1000万 × 365 = 3.65亿条

二、容量估算(5分钟)

1️⃣ QPS估算

写入QPS(生成短链):

平均写QPS = 10,000,000(生成)/ 86,400秒 ≈ 116 QPS
峰值写QPS = 116 × 3(峰值系数)≈ 350 QPS
保守估计 = 1,000 QPS(留余量)

读取QPS(跳转):

平均读QPS = 100,000,000(跳转)/ 86,400秒 ≈ 1,157 QPS
峰值读QPS = 1,157 × 3 ≈ 3,500 QPS
保守估计 = 10,000 QPS(留余量)

读写比例:10:1(典型的读多写少场景)

2️⃣ 存储估算

单条记录大小:

短链映射表(url_mapping):
- id: BIGINT = 8 bytes
- short_code: CHAR(8) = 8 bytes
- long_url: VARCHAR(2048) = 2048 bytes(平均500 bytes)
- user_id: BIGINT = 8 bytes
- click_count: INT = 4 bytes
- created_at: TIMESTAMP = 8 bytes
- expire_at: TIMESTAMP = 8 bytes

单条记录:8 + 8 + 500 + 8 + 4 + 8 + 8 = 544 bytes ≈ 0.5KB

总存储:

裸数据:3.65亿 × 0.5KB = 182.5 GB
索引开销(30%):182.5 × 0.3 = 54.75 GB
主从复制(2倍):(182.5 + 54.75) × 2 = 474.5 GB
总计:约 500 GB(1年数据)

结论:单机MySQL足够(一般配2TB磁盘)

3️⃣ 带宽估算

跳转请求:

请求大小:
- HTTP Header: 0.5 KB
- Short URL: 0.1 KB
总计:0.6 KB

带宽:10,000 QPS × 0.6 KB = 6 MB/s = 48 Mbps

跳转响应:

响应大小:
- HTTP 302 Header: 0.5 KB
- Location URL: 2 KB
总计:2.5 KB

带宽:10,000 QPS × 2.5 KB = 25 MB/s = 200 Mbps

总带宽:48 + 200 = 248 Mbps → 需要 500 Mbps 带宽


🔌 三、接口设计(5分钟)

1️⃣ RESTful API设计

生成短链

POST /api/v1/shorten
Content-Type: application/json

Request Body:
{
  "long_url": "https://www.example.com/very/long/url?param1=value1&param2=value2",
  "custom_alias": "my-link",          // 可选,自定义短链码
  "expire_at": "2024-12-31T23:59:59Z" // 可选,过期时间
}

Response (200 OK):
{
  "code": 0,
  "message": "success",
  "data": {
    "short_url": "https://short.ly/abc123",
    "short_code": "abc123",
    "long_url": "https://www.example.com/very/long/url?param1=value1&param2=value2",
    "created_at": "2024-01-01T00:00:00Z",
    "expire_at": "2024-12-31T23:59:59Z"
  }
}

Error Response (400 Bad Request):
{
  "code": 1001,
  "message": "invalid URL format",
  "data": null
}

Error Response (409 Conflict):
{
  "code": 2001,
  "message": "custom alias already exists",
  "data": null
}

短链跳转

GET /{short_code}

Response (302 Found):
HTTP/1.1 302 Found
Location: https://www.example.com/very/long/url?param1=value1&param2=value2
Cache-Control: public, max-age=300

Response (404 Not Found):
HTTP/1.1 404 Not Found
Content-Type: text/html

<!DOCTYPE html>
<html>
<head><title>404 Not Found</title></head>
<body><h1>短链不存在或已过期</h1></body>
</html>

查询统计

GET /api/v1/stats/{short_code}
Authorization: Bearer <token>

Response (200 OK):
{
  "code": 0,
  "data": {
    "short_code": "abc123",
    "long_url": "https://www.example.com/very/long/url",
    "total_clicks": 12345,
    "created_at": "2024-01-01T00:00:00Z",
    "daily_stats": [
      {"date": "2024-01-01", "clicks": 100},
      {"date": "2024-01-02", "clicks": 150},
      {"date": "2024-01-03", "clicks": 200}
    ]
  }
}

2️⃣ 错误码设计

0:    成功
1xxx: 客户端错误
  1001: 参数错误
  1002: URL格式错误
  1003: 未授权

2xxx: 业务错误
  2001: 自定义短链已存在
  2002: 短链不存在
  2003: 短链已过期
  2004: 短链已被删除

3xxx: 服务器错误
  3001: 数据库错误
  3002: 缓存错误
  3003: 系统繁忙

🗄️ 四、数据模型设计(5分钟)

1️⃣ 核心表设计

url_mapping(短链映射表)

CREATE TABLE url_mapping (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    short_code CHAR(8) NOT NULL COMMENT '短链码(Base62编码)',
    long_url VARCHAR(2048) NOT NULL COMMENT '原始长URL',
    user_id BIGINT UNSIGNED DEFAULT NULL COMMENT '创建用户ID',
    click_count INT UNSIGNED DEFAULT 0 COMMENT '点击次数(冗余字段,用于快速查询)',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    expire_at TIMESTAMP NULL COMMENT '过期时间,NULL表示永不过期',
    is_deleted TINYINT DEFAULT 0 COMMENT '软删除标记,0=正常 1=已删除',

    UNIQUE KEY uk_short_code (short_code),
    KEY idx_user_id (user_id),
    KEY idx_created_at (created_at),
    KEY idx_expire_at (expire_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='短链映射表';

索引说明:

  • uk_short_code:唯一索引,保证短链码唯一,支持跳转查询
  • idx_user_id:支持用户维度查询(我的短链列表)
  • idx_created_at:支持时间范围查询
  • idx_expire_at:支持定时任务扫描过期短链

url_click_log(点击日志表)

CREATE TABLE url_click_log (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    short_code CHAR(8) NOT NULL COMMENT '短链码',
    ip_address VARCHAR(45) COMMENT '访问者IP',
    user_agent TEXT COMMENT '浏览器UA',
    referer VARCHAR(1024) COMMENT '来源页面',
    country VARCHAR(50) COMMENT '国家',
    city VARCHAR(100) COMMENT '城市',
    device_type VARCHAR(20) COMMENT '设备类型:mobile/desktop/tablet',
    clicked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '点击时间',

    KEY idx_short_code (short_code),
    KEY idx_clicked_at (clicked_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='点击日志表';

优化说明:

  • 点击日志表数据量巨大,可以按月分表:url_click_log_202401
  • 历史数据可以归档到对象存储(OSS、S3)
  • 实时统计用Redis,离线统计用Kafka + ClickHouse

url_stats(统计汇总表)

CREATE TABLE url_stats (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    short_code CHAR(8) NOT NULL COMMENT '短链码',
    stat_date DATE NOT NULL COMMENT '统计日期',
    click_count INT UNSIGNED DEFAULT 0 COMMENT '当日点击次数',
    unique_visitors INT UNSIGNED DEFAULT 0 COMMENT '当日独立访客数',

    UNIQUE KEY uk_short_code_date (short_code, stat_date),
    KEY idx_stat_date (stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='按日统计汇总表';

2️⃣ 分库分表策略

何时需要分库分表?

  • 单表数据量超过 1000万行
  • 单表磁盘文件超过 2GB
  • 查询性能明显下降

分表方案:

方案1:按短链码Hash分表(推荐)

-- 100张表:url_mapping_00 ~ url_mapping_99
表名计算:table_index = crc32(short_code) % 100
表名:url_mapping_{table_index}

优点:
- 数据分布均匀
- 跳转查询只需访问一张表
- 水平扩展容易

缺点:
- 用户维度查询需要扫描所有表

方案2:按时间范围分表

-- 按月分表:url_mapping_202401, url_mapping_202402, ...
表名:url_mapping_{YYYYMM}

优点:
- 历史数据归档方便
- 删除过期数据直接删表

缺点:
- 热点数据集中在最新表
- 跳转查询需要知道创建时间

推荐方案:

- url_mapping:按 short_code Hash 分 100 张表(支持高并发跳转)
- url_click_log:按月分表(支持历史数据归档)
- url_stats:按日期分表(支持快速查询统计)

🏗️ 五、架构设计(20分钟)

1️⃣ V1:单体架构(MVP)

适用场景:

  • 初创公司,快速验证MVP
  • 日活 < 10万,QPS < 100
┌─────────────────────────────────────────────────────────────┐
│                      V1:单体架构                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────┐                                               │
│  │  浏览器   │                                               │
│  └────┬─────┘                                               │
│       │ HTTPS                                               │
│                                                            │
│  ┌─────────────┐                                            │
│  │   Nginx     │ (Web服务器 + 反向代理)                      │
│  └──────┬──────┘                                            │
│         │                                                   │
│                                                            │
│  ┌─────────────┐                                            │
│  │ Application │                                            │
│  │   (Go)      │                                            │
│  │             │                                            │
│  │ ┌─────────┐ │                                            │
│  │ │生成短链  │ │  - Base62编码                             │
│  │ └─────────┘ │  - 唯一性检查                             │
│  │             │                                            │
│  │ ┌─────────┐ │                                            │
│  │ │短链跳转  │ │  - 查询映射                               │
│  │ └─────────┘ │  - 302重定向                              │
│  │             │                                            │
│  │ ┌─────────┐ │                                            │
│  │ │统计更新  │ │  - 点击量+1                               │
│  │ └─────────┘ │                                            │
│  └──────┬──────┘                                            │
│         │                                                   │
│                                                            │
│  ┌─────────────┐                                            │
│  │   MySQL     │                                            │
│  │  (Master)   │  - url_mapping                            │
│  │             │  - url_stats                              │
│  └─────────────┘                                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

核心流程:
1. 生成短链:
   - 接收长URL
   - 生成短链码(自增ID → Base62编码)
   - 写入MySQL
   - 返回短URL

2. 短链跳转:
   - 解析短链码
   - 查询MySQL获取长URL
   - 更新点击量(UPDATE click_count = click_count + 1)
   - 302重定向

优点:
 实现简单,快速上线
 单机部署,运维成本低
 适合MVP验证

缺点:
 性能瓶颈:数据库单点,QPS < 1000
 可用性差:任何组件故障都不可用
 无缓存:所有请求都打到DB
 统计更新慢:同步更新阻塞请求

2️⃣ V2:读写分离 + 缓存优化

适用场景:

  • 成长期公司,流量增长
  • 日活 10万-100万,QPS 1000-5000
┌─────────────────────────────────────────────────────────────┐
│              V2:读写分离 + 缓存优化                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────┐                                               │
│  │  浏览器   │                                               │
│  └────┬─────┘                                               │
│       │                                                     │
│                                                            │
│  ┌─────────────┐                                            │
│  │Load Balancer│ (Nginx/LVS)                               │
│  └──────┬──────┘                                            │
│         │                                                   │
│         ├──────────┬──────────┬──────────┐                 │
│                                                        │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐                   │
│  │  App 1   │ │  App 2   │ │  App N   │                   │
│  └────┬─────┘ └────┬─────┘ └────┬─────┘                   │
│       │            │            │                          │
│       └────────┬───┴────────────┘                          │
│                │                                            │
│    ┌───────────┼───────────────┬─────────────┐             │
│    │           │               │             │             │
│                                                        │
│ ┌────────┐ ┌────────┐  ┌──────────┐  ┌──────────┐         │
│ │ Redis  │ │ MySQL  │  │  MySQL   │  │  Kafka   │         │
│ │(缓存)  │ │(Master)│  │ (Slave)  │  │ (消息队列)│         │
│ └────────┘ └────┬───┘  └────┬─────┘  └──────────┘         │
│     │           │            │                             │
│     │           └────────────┘ (主从复制)                   │
│     │                                                       │
│     │  Cache结构:                                          │
│     │  key: "short:{short_code}"                           │
│     │  value: "long_url"                                   │
│     │  TTL: 3600s                                          │
│     │                                                       │
│     └─────────────────────────────────────────────────────┐│
│                                                            ││
│  ┌──────────────────────────────────────────────────────┐ ││
│  │             统计消费者集群                            │ ││
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐           │ ││
│  │  │Consumer 1│  │Consumer 2│  │Consumer N│           │ ││
│  │  └────┬─────┘  └────┬─────┘  └────┬─────┘           │ ││
│  │       │             │             │                  │ ││
│  │       └─────────────┴─────────────┘                  │ ││
│  │                     │                                │ ││
│  │                                                     │ ││
│  │              ┌──────────┐                            │ ││
│  │              │  MySQL   │  批量更新统计数据           │ ││
│  │              │(统计库)  │                            │ ││
│  │              └──────────┘                            │ ││
│  └──────────────────────────────────────────────────────┘ ││
│                                                          ││
│                               └───────────────────────────┘│
│                                                             │
└─────────────────────────────────────────────────────────────┘

核心优化:

1. 引入Redis缓存(命中率 > 95%):
   - 跳转流程:先查Redis,未命中再查DB
   - 缓存策略:LRU淘汰,TTL=1小时
   - 缓存预热:启动时加载热点数据

2. 读写分离:
   - 主库:写入(生成短链)
   - 从库:读取(跳转查询)
   - 主从延迟监控:< 1s

3. 异步统计(Kafka):
   - 点击事件发送到Kafka
   - 消费者批量更新统计数据
   - 解耦请求链路,提升性能

4. 应用层水平扩展:
   - 无状态设计
   - 负载均衡(轮询/最小连接)
   - 支持弹性伸缩

性能提升:
 QPS提升 10-50倍(缓存命中后)
 响应时间降低 90%(P99 < 50ms)
 数据库压力降低 95%

改进点:
 引入缓存,读性能大幅提升
 读写分离,写操作不影响读
 异步统计,请求链路解耦
 应用层水平扩展

待优化:
 主库仍是单点,写能力有限
 缓存一致性问题(主从延迟)
 单机Redis内存有限
 跨地域访问延迟高

3️⃣ V3:分库分表 + 高可用

适用场景:

  • 成熟公司,千万级DAU
  • QPS > 10000,数据量 > 1亿
┌─────────────────────────────────────────────────────────────┐
│           V3:分库分表 + 高可用 + 多机房部署                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                   CDN层                              │   │
│  │  - 静态资源加速(404页面、二维码图片)                 │   │
│  │  - 边缘节点缓存热点短链                               │   │
│  └──────────────────────┬──────────────────────────────┘   │
│                         │                                   │
│  ┌──────────────────────┴──────────────────────────────┐   │
│  │              全局负载均衡(GSLB)                     │   │
│  │  - DNS智能解析(GeoDNS)                             │   │
│  │  - 就近接入(延迟最优)                               │   │
│  └──────┬───────────────────┬───────────────────┬───────┘   │
│         │                   │                   │           │
│  ┌──────┴───────┐   ┌───────┴────────┐  ┌──────┴────────┐  │
│  │   机房A      │   │    机房B        │  │    机房C       │  │
│  │   (华北)     │   │   (华东)        │  │   (华南)       │  │
│  └──────┬───────┘   └───────┬────────┘  └──────┬────────┘  │
│         │                   │                   │           │
│                                                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │            应用层(水平扩展)                        │   │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐           │   │
│  │  │ App Pod  │  │ App Pod  │  │ App Pod  │  ...      │   │
│  │  └────┬─────┘  └────┬─────┘  └────┬─────┘           │   │
│  │       │             │             │                  │   │
│  │       └─────────────┴─────────────┘                  │   │
│  └───────────────────────┬─────────────────────────────┘   │
│                          │                                  │
│  ┌───────────────────────┼─────────────────────────────┐   │
│  │           中间件层(高可用集群)                      │   │
│  │                       │                               │   │
│  │  ┌────────────────────┴────────────────────────┐     │   │
│  │  │         Redis集群(哨兵模式)                │     │   │
│  │  │  ┌────────┐  ┌────────┐  ┌────────┐         │     │   │
│  │  │  │Master 1│  │Master 2│  │Master N│         │     │   │
│  │  │  └───┬────┘  └───┬────┘  └───┬────┘         │     │   │
│  │  │      │           │           │              │     │   │
│  │  │  ┌───┴────┐  ┌───┴────┐  ┌───┴────┐         │     │   │
│  │  │  │ Slave  │  │ Slave  │  │ Slave  │         │     │   │
│  │  │  └────────┘  └────────┘  └────────┘         │     │   │
│  │  │                                              │     │   │
│  │  │  分片策略:hash(short_code) % 16             │     │   │
│  │  └──────────────────────────────────────────────┘     │   │
│  │                       │                               │   │
│  │  ┌────────────────────┴────────────────────────┐     │   │
│  │  │          Kafka集群(3副本)                  │     │   │
│  │  │  - 点击事件流                                │     │   │
│  │  │  - 统计任务队列                              │     │   │
│  │  └──────────────────────────────────────────────┘     │   │
│  └───────────────────────┬─────────────────────────────┘   │
│                          │                                  │
│  ┌───────────────────────┼─────────────────────────────┐   │
│  │          数据层(分库分表 + 主从)                    │   │
│  │                       │                               │   │
│  │  ┌────────────────────┴────────────────────────┐     │   │
│  │  │       MySQL集群(分片 0-99)                 │     │   │
│  │  │                                              │     │   │
│  │  │  Shard 0        Shard 1  ...   Shard 99     │     │   │
│  │  │  ┌────────┐    ┌────────┐    ┌────────┐    │     │   │
│  │  │  │Master 0│    │Master 1│    │Master N│    │     │   │
│  │  │  └───┬────┘    └───┬────┘    └───┬────┘    │     │   │
│  │  │      │             │             │          │     │   │
│  │  │  ┌───┴───┐     ┌───┴───┐     ┌───┴───┐     │     │   │
│  │  │  │Slave 1│     │Slave 1│     │Slave 1│     │     │   │
│  │  │  └───────┘     └───────┘     └───────┘     │     │   │
│  │  │  ┌───────┐     ┌───────┐     ┌───────┐     │     │   │
│  │  │  │Slave 2│     │Slave 2│     │Slave 2│     │     │   │
│  │  │  └───────┘     └───────┘     └───────┘     │     │   │
│  │  │                                              │     │   │
│  │  │  路由规则:shard_id = crc32(short_code) % 100│     │   │
│  │  └──────────────────────────────────────────────┘     │   │
│  │                       │                               │   │
│  │  ┌────────────────────┴────────────────────────┐     │   │
│  │  │      ClickHouse集群(OLAP分析)              │     │   │
│  │  │  - 点击日志存储                              │     │   │
│  │  │  - 实时统计分析                              │     │   │
│  │  └──────────────────────────────────────────────┘     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

核心架构特点:

1. 多机房部署(容灾):
   - 3个机房:华北、华东、华南
   - GSLB智能解析:就近接入
   - 跨机房数据同步:Binlog + MQ

2. 分库分表(突破单机瓶颈):
   - MySQL分100张表
   - 路由规则:crc32(short_code) % 100
   - 支持在线扩容(双写 + 迁移)

3. Redis集群(缓存高可用):
   - 16个分片(主从 + 哨兵)
   - 每个分片:1主2从
   - 自动故障转移:< 30s

4. Kafka集群(异步解耦):
   - 3个Broker,3副本
   - 点击事件实时处理
   - 消费者组并行消费

5. ClickHouse(OLAP分析):
   - 存储点击日志(按月分区)
   - 实时统计分析(秒级)
   - 支持复杂查询

性能指标:
 QPS:50,000+(峰值)
 响应时间:P99 < 50ms
 可用性:99.99%
 数据量:10亿+ 条

高可用保证:
 应用层:无状态,水平扩展
 缓存层:主从 + 哨兵,自动故障转移
 数据库:主从 + 分片,读写分离
 消息队列:多副本,持久化
 多机房:跨地域容灾

️ 六、核心算法:短链生成

短链生成是整个系统的核心算法,直接影响性能和可扩展性。

1️⃣ 算法对比

算法原理优点缺点适用场景
自增ID + Base62数据库自增ID转Base62简单、唯一、有序依赖DB、可预测小规模系统
哈希算法MD5/SHA256截取前N位无需DB、性能高可能冲突、需重试无状态系统
雪花算法时间戳+机器ID+序列号分布式、唯一、性能高时钟回拨问题大规模分布式
预生成ID池提前生成ID存Redis性能极高需维护ID池高并发场景

2️⃣ 方案1:自增ID + Base62(推荐MVP)

原理:

1. 使用MySQL自增ID(BIGINT)
2. ID转换为Base62字符串(62进制:0-9a-zA-Z)
3. Base62字符串作为短链码

示例:
ID = 1234567890
Base62 = "1LY7VK" (6位)
Short URL = "https://short.ly/1LY7VK"

Base62编码实现:

package shorturl

import (
    "math"
    "strings"
)

const (
    // Base62字符集:0-9a-zA-Z(共62个字符)
    base62Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    base        = 62
)

// IDToBase62 将数字ID转换为Base62字符串
func IDToBase62(id uint64) string {
    if id == 0 {
        return string(base62Chars[0])
    }

    var result []byte
    for id > 0 {
        remainder := id % base
        result = append([]byte{base62Chars[remainder]}, result...)
        id = id / base
    }

    return string(result)
}

// Base62ToID 将Base62字符串转换为数字ID
func Base62ToID(code string) (uint64, error) {
    var id uint64
    for i, char := range code {
        pos := strings.IndexRune(base62Chars, char)
        if pos == -1 {
            return 0, fmt.Errorf("invalid character: %c", char)
        }

        power := len(code) - i - 1
        id += uint64(pos) * uint64(math.Pow(base, float64(power)))
    }

    return id, nil
}

// 示例:
// ID = 1234567890
// Base62 = IDToBase62(1234567890) = "1LY7VK"
// ID = Base62ToID("1LY7VK") = 1234567890

长度计算:

Base62长度与ID范围的关系:
- 6位:62^6 = 56,800,235,584(568亿)
- 7位:62^7 = 3,521,614,606,208(3.5万亿)
- 8位:62^8 = 218,340,105,584,896(218万亿)

结论:7位Base62足够使用(支持3.5万亿条短链)

完整流程:

// 生成短链
func (s *Service) GenerateShortURL(longURL string) (string, error) {
    // 1. 插入数据库,获取自增ID
    result, err := s.db.Exec(
        "INSERT INTO url_mapping (long_url, created_at) VALUES (?, NOW())",
        longURL,
    )
    if err != nil {
        return "", err
    }

    // 2. 获取自增ID
    id, err := result.LastInsertId()
    if err != nil {
        return "", err
    }

    // 3. ID转Base62
    shortCode := IDToBase62(uint64(id))

    // 4. 更新short_code字段
    _, err = s.db.Exec(
        "UPDATE url_mapping SET short_code = ? WHERE id = ?",
        shortCode, id,
    )
    if err != nil {
        return "", err
    }

    // 5. 写入缓存
    s.redis.SetEx(
        ctx,
        fmt.Sprintf("short:%s", shortCode),
        longURL,
        3600 * time.Second, // TTL = 1小时
    )

    // 6. 返回短链
    return fmt.Sprintf("https://short.ly/%s", shortCode), nil
}

优点: 实现简单,逻辑清晰 全局唯一,无需担心冲突 有序递增,便于分库分表 长度可控(7位Base62)

缺点: 依赖数据库自增ID 短链码可预测(安全性低) 分布式扩展困难(多实例ID冲突)

3️⃣ 方案2:雪花算法(推荐大规模)

雪花算法结构(64位):

┌─────────────────────────────────────────────────────────────┐
│                   雪花算法 64位结构                          │
├─────────────────────────────────────────────────────────────┤
│ 1位符号位 │ 41位时间戳 │ 10位机器ID │ 12位序列号 │          │
│  (固定0)  │ (毫秒级)   │ (1024台)  │ (4096/ms) │          │
├─────────────────────────────────────────────────────────────┤
│    0      │ 41 bits    │ 10 bits    │ 12 bits    │          │
└─────────────────────────────────────────────────────────────┘

特点:
- 时间戳(41位):可用69年(2^41 / 1000 / 60 / 60 / 24 / 365 ≈ 69)
- 机器ID(10位):支持1024个节点
- 序列号(12位):每毫秒可生成4096个ID
- QPS:4096 * 1000 = 409万/秒(单机)

Go语言实现:

package snowflake

import (
    "errors"
    "sync"
    "time"
)

const (
    epoch            = int64(1609459200000) // 2021-01-01 00:00:00 UTC (毫秒)
    timestampBits    = 41
    machineIDBits    = 10
    sequenceBits     = 12

    maxMachineID     = -1 ^ (-1 << machineIDBits) // 1023
    maxSequence      = -1 ^ (-1 << sequenceBits)  // 4095

    machineIDShift   = sequenceBits
    timestampShift   = sequenceBits + machineIDBits
)

type Snowflake struct {
    mu           sync.Mutex
    timestamp    int64
    machineID    int64
    sequence     int64
}

func NewSnowflake(machineID int64) (*Snowflake, error) {
    if machineID < 0 || machineID > maxMachineID {
        return nil, errors.New("invalid machine ID")
    }

    return &Snowflake{
        timestamp: 0,
        machineID: machineID,
        sequence:  0,
    }, nil
}

func (s *Snowflake) NextID() (uint64, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    now := time.Now().UnixNano() / 1e6 // 毫秒

    if now < s.timestamp {
        // 时钟回拨,等待追上
        return 0, errors.New("clock moved backwards")
    }

    if now == s.timestamp {
        // 同一毫秒内,序列号递增
        s.sequence = (s.sequence + 1) & maxSequence
        if s.sequence == 0 {
            // 序列号用完,等待下一毫秒
            for now <= s.timestamp {
                now = time.Now().UnixNano() / 1e6
            }
        }
    } else {
        // 新的一毫秒,序列号重置为0
        s.sequence = 0
    }

    s.timestamp = now

    // 组合生成ID
    id := ((now - epoch) << timestampShift) |
          (s.machineID << machineIDShift) |
          s.sequence

    return uint64(id), nil
}

// 使用示例:
// snowflake, _ := NewSnowflake(1) // 机器ID=1
// id, _ := snowflake.NextID()     // 生成ID:1234567890123456789
// shortCode := IDToBase62(id)     // 转Base62:"2lkQW7xYZ"

优点: 分布式友好,无需协调 性能极高(409万QPS单机) 全局唯一,趋势递增 包含时间信息,可排序

缺点: 时钟回拨问题(需处理) 依赖机器ID分配(需中心化管理) 长度较长(19位数字 → 11位Base62)

时钟回拨解决方案:

// 方案1:等待时钟追上(最多等2秒)
if now < s.timestamp {
    offset := s.timestamp - now
    if offset > 2000 { // 超过2秒,报错
        return 0, errors.New("clock moved backwards too much")
    }
    time.Sleep(time.Duration(offset) * time.Millisecond)
    now = time.Now().UnixNano() / 1e6
}

// 方案2:使用备用机器ID(预留多个机器ID)
if now < s.timestamp {
    s.machineID = (s.machineID + 1) % maxMachineID
}

// 方案3:记录最后时间戳到Redis(重启后恢复)
lastTimestamp, _ := redis.Get(ctx, "snowflake:last_timestamp").Int64()
if now <= lastTimestamp {
    now = lastTimestamp + 1
}
redis.Set(ctx, "snowflake:last_timestamp", now, 0)

七、核心流程详解

1️⃣ 生成短链流程

func (s *Service) CreateShortURL(req *CreateRequest) (*CreateResponse, error) {
    // 1. 参数校验
    if !isValidURL(req.LongURL) {
        return nil, errors.New("invalid URL")
    }

    // 2. 检查是否已存在(去重)
    if existing, err := s.findExisting(req.LongURL); err == nil {
        return &CreateResponse{
            ShortURL: existing.ShortURL,
            ShortCode: existing.ShortCode,
        }, nil
    }

    // 3. 生成短链码
    var shortCode string
    if req.CustomAlias != "" {
        // 自定义短链:检查是否可用
        if s.isAliasAvailable(req.CustomAlias) {
            shortCode = req.CustomAlias
        } else {
            return nil, errors.New("alias already exists")
        }
    } else {
        // 自动生成:雪花算法
        id, err := s.snowflake.NextID()
        if err != nil {
            return nil, err
        }
        shortCode = IDToBase62(id)
    }

    // 4. 写入数据库
    mapping := &URLMapping{
        ShortCode: shortCode,
        LongURL:   req.LongURL,
        UserID:    req.UserID,
        ExpireAt:  req.ExpireAt,
        CreatedAt: time.Now(),
    }

    if err := s.db.Create(mapping); err != nil {
        return nil, err
    }

    // 5. 写入缓存(可选,预热热点数据)
    s.redis.SetEx(
        ctx,
        fmt.Sprintf("short:%s", shortCode),
        req.LongURL,
        3600 * time.Second,
    )

    // 6. 返回结果
    return &CreateResponse{
        ShortURL:  fmt.Sprintf("https://short.ly/%s", shortCode),
        ShortCode: shortCode,
        LongURL:   req.LongURL,
        CreatedAt: mapping.CreatedAt,
        ExpireAt:  mapping.ExpireAt,
    }, nil
}

2️⃣ 短链跳转流程(重点优化)

func (s *Service) RedirectShortURL(shortCode string) (string, error) {
    // 1. 参数校验
    if len(shortCode) < 6 || len(shortCode) > 10 {
        return "", errors.New("invalid short code")
    }

    // 2. 查询缓存(Redis)
    cacheKey := fmt.Sprintf("short:%s", shortCode)
    longURL, err := s.redis.Get(ctx, cacheKey).Result()
    if err == nil && longURL != "" {
        // 缓存命中,异步更新统计
        s.asyncUpdateStats(shortCode)
        return longURL, nil
    }

    // 3. 缓存未命中,查询数据库
    mapping, err := s.db.FindByShortCode(shortCode)
    if err != nil {
        // 短链不存在
        return "", errors.New("short URL not found")
    }

    // 4. 检查是否过期
    if mapping.ExpireAt != nil && mapping.ExpireAt.Before(time.Now()) {
        return "", errors.New("short URL expired")
    }

    // 5. 更新缓存
    s.redis.SetEx(
        ctx,
        cacheKey,
        mapping.LongURL,
        3600 * time.Second, // TTL = 1小时
    )

    // 6. 异步更新统计
    s.asyncUpdateStats(shortCode)

    // 7. 返回长URL
    return mapping.LongURL, nil
}

// 异步更新统计(不阻塞请求)
func (s *Service) asyncUpdateStats(shortCode string) {
    // 发送点击事件到Kafka
    event := &ClickEvent{
        ShortCode: shortCode,
        Timestamp: time.Now(),
        IP:        getClientIP(),
        UserAgent: getUserAgent(),
    }

    s.kafka.Produce("url_click_events", event)
}

性能优化要点:

1. 缓存优先:
   - 95%请求命中Redis(P99 < 5ms)
   - 5%请求查询DB(P99 < 50ms)

2. 异步统计:
   - 点击事件发送到Kafka(耗时 < 1ms)
   - 消费者批量更新DB(解耦请求链路)

3. 布隆过滤器(防穿透):
   - 拦截不存在的short_code
   - 避免恶意查询打穿缓存

4. 缓存预热:
   - 系统启动时加载热点数据
   - 定时任务更新热点排行榜

3️⃣ 统计更新流程

// Kafka消费者:批量更新统计
func (s *Consumer) ConsumeClickEvents() {
    batch := make([]*ClickEvent, 0, 1000)
    ticker := time.NewTicker(5 * time.Second)

    for {
        select {
        case event := <-s.kafka.Consume("url_click_events"):
            batch = append(batch, event)

            // 达到批量大小,立即刷新
            if len(batch) >= 1000 {
                s.flushBatch(batch)
                batch = batch[:0]
            }

        case <-ticker.C:
            // 定时刷新(即使未达到批量大小)
            if len(batch) > 0 {
                s.flushBatch(batch)
                batch = batch[:0]
            }
        }
    }
}

func (s *Consumer) flushBatch(events []*ClickEvent) error {
    // 1. 按short_code聚合
    stats := make(map[string]int)
    for _, event := range events {
        stats[event.ShortCode]++
    }

    // 2. 批量更新数据库
    tx, _ := s.db.Begin()
    for shortCode, count := range stats {
        // 更新总点击量(url_mapping表)
        tx.Exec(
            "UPDATE url_mapping SET click_count = click_count + ? WHERE short_code = ?",
            count, shortCode,
        )

        // 更新按日统计(url_stats表)
        today := time.Now().Format("2006-01-02")
        tx.Exec(`
            INSERT INTO url_stats (short_code, stat_date, click_count)
            VALUES (?, ?, ?)
            ON DUPLICATE KEY UPDATE click_count = click_count + VALUES(click_count)
        `, shortCode, today, count)
    }
    tx.Commit()

    // 3. 更新Redis缓存的点击量
    for shortCode, count := range stats {
        s.redis.IncrBy(
            ctx,
            fmt.Sprintf("clicks:%s", shortCode),
            int64(count),
        )
    }

    return nil
}

八、性能优化方案

1️⃣ 缓存优化

多级缓存架构

┌─────────────────────────────────────────────────────────────┐
│                  多级缓存架构                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  L1: 浏览器缓存(Browser Cache)                             │
│      Cache-Control: public, max-age=300                     │
│      命中率:10%,延迟:0ms                                   │
│      │                                                       │
│                                                             │
│  L2: CDN边缘节点(Edge Cache)                               │
│      缓存热点短链404页面、二维码                              │
│      命中率:20%,延迟:5-10ms                                │
│      │                                                       │
│                                                             │
│  L3: 应用层本地缓存(Local Cache)                            │
│      LRU缓存,size=10000                                    │
│      命中率:30%,延迟:< 1ms                                 │
│      │                                                       │
│                                                             │
│  L4: Redis分布式缓存(Redis Cluster)                        │
│      全量热点数据                                            │
│      命中率:95%,延迟:1-5ms                                 │
│      │                                                       │
│                                                             │
│  L5: MySQL数据库(MySQL Cluster)                           │
│      持久化存储                                              │
│      命中率:5%,延迟:10-50ms                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

缓存策略

// 缓存穿透:布隆过滤器
type BloomFilter struct {
    bits []uint64
    size uint64
    hashFuncs int
}

func (bf *BloomFilter) Add(shortCode string) {
    for i := 0; i < bf.hashFuncs; i++ {
        hash := bf.hash(shortCode, i)
        index := hash % bf.size
        bf.bits[index/64] |= (1 << (index % 64))
    }
}

func (bf *BloomFilter) Exists(shortCode string) bool {
    for i := 0; i < bf.hashFuncs; i++ {
        hash := bf.hash(shortCode, i)
        index := hash % bf.size
        if bf.bits[index/64]&(1<<(index%64)) == 0 {
            return false // 一定不存在
        }
    }
    return true // 可能存在
}

// 使用布隆过滤器
func (s *Service) RedirectWithBloomFilter(shortCode string) (string, error) {
    // 1. 布隆过滤器检查
    if !s.bloomFilter.Exists(shortCode) {
        return "", errors.New("short URL not found")
    }

    // 2. 查询缓存
    // 3. 查询数据库
    // ...
}

缓存击穿:互斥锁

// 缓存击穿:热点key过期,大量请求打到DB
func (s *Service) GetWithMutex(shortCode string) (string, error) {
    // 1. 查询缓存
    longURL, err := s.redis.Get(ctx, cacheKey).Result()
    if err == nil {
        return longURL, nil
    }

    // 2. 缓存未命中,加锁
    lockKey := fmt.Sprintf("lock:%s", shortCode)
    locked, _ := s.redis.SetNX(ctx, lockKey, "1", 10*time.Second).Result()

    if locked {
        // 获取锁成功,查询DB
        defer s.redis.Del(ctx, lockKey)

        mapping, err := s.db.FindByShortCode(shortCode)
        if err != nil {
            return "", err
        }

        // 更新缓存
        s.redis.SetEx(ctx, cacheKey, mapping.LongURL, 3600*time.Second)

        return mapping.LongURL, nil
    } else {
        // 获取锁失败,等待并重试
        time.Sleep(50 * time.Millisecond)
        return s.redis.Get(ctx, cacheKey).Result()
    }
}

缓存雪崩:过期时间加随机值

// 缓存雪崩:大量key同时过期
func (s *Service) SetCacheWithRandomTTL(key string, value string, baseTTL int) {
    // TTL = baseTTL ± 20%
    randomOffset := rand.Intn(baseTTL / 5)
    ttl := baseTTL + randomOffset - baseTTL/10

    s.redis.SetEx(ctx, key, value, time.Duration(ttl)*time.Second)
}

2️⃣ 数据库优化

索引优化

-- 覆盖索引:查询只需访问索引,不回表
SELECT long_url FROM url_mapping WHERE short_code = 'abc123';

-- 建立覆盖索引
CREATE INDEX idx_short_code_long_url ON url_mapping(short_code, long_url);

-- 联合索引:最左前缀原则
CREATE INDEX idx_user_created ON url_mapping(user_id, created_at);

-- 可以使用的查询:
SELECT * FROM url_mapping WHERE user_id = 123;  -- 
SELECT * FROM url_mapping WHERE user_id = 123 AND created_at > '2024-01-01';  -- 
SELECT * FROM url_mapping WHERE created_at > '2024-01-01';  --  不走索引

分页优化

--  深分页性能差:OFFSET很大时,需要扫描大量数据
SELECT * FROM url_mapping ORDER BY id LIMIT 1000000, 20;

--  使用上次最大ID优化(游标分页)
SELECT * FROM url_mapping WHERE id > 1000000 ORDER BY id LIMIT 20;

批量操作优化

//  逐条插入:1000次DB调用
for _, url := range urls {
    db.Exec("INSERT INTO url_mapping (long_url) VALUES (?)", url)
}

//  批量插入:1次DB调用
values := make([]string, 0, len(urls))
args := make([]interface{}, 0, len(urls))
for _, url := range urls {
    values = append(values, "(?)")
    args = append(args, url)
}
sql := fmt.Sprintf("INSERT INTO url_mapping (long_url) VALUES %s",
    strings.Join(values, ","))
db.Exec(sql, args...)

3️⃣ 异步化改造

同步 → 异步的典型场景:

1. 点击量统计:
   同步:UPDATE click_count(阻塞请求,影响性能)
   异步:发送Kafka消息(< 1ms,不阻塞)

2. 日志记录:
   同步:写入MySQL日志表(IO阻塞)
   异步:写入Kafka → 批量写ClickHouse

3. 通知推送:
   同步:调用第三方API(网络延迟高)
   异步:任务队列 → Worker异步执行

4. 二维码生成:
   同步:实时生成二维码(CPU密集)
   异步:后台任务生成,返回占位图

九、监控与告警

1️⃣ 核心指标

业务指标:
- 短链生成QPS
- 短链跳转QPS
- 短链转化率(点击/生成)
- Top 100热点短链

性能指标:
- API响应时间(P50/P95/P99)
- 缓存命中率
- 数据库慢查询
- Kafka消费延迟

可用性指标:
- 系统可用性(SLA)
- 错误率(4xx/5xx)
- 失败请求数

资源指标:
- CPU使用率
- 内存使用率
- 磁盘使用率
- 网络带宽

2️⃣ 告警规则

# Prometheus告警规则
groups:
  - name: shorturl_alerts
    rules:
      # QPS异常
      - alert: HighQPS
        expr: rate(http_requests_total[1m]) > 20000
        for: 5m
        annotations:
          summary: "QPS过高: {{ $value }}"

      # 错误率异常
      - alert: HighErrorRate
        expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.01
        for: 2m
        annotations:
          summary: "错误率过高: {{ $value }}"

      # 缓存命中率低
      - alert: LowCacheHitRate
        expr: redis_hit_rate < 0.90
        for: 10m
        annotations:
          summary: "缓存命中率过低: {{ $value }}"

      # 数据库连接池耗尽
      - alert: DBConnectionPoolExhausted
        expr: mysql_connection_pool_used / mysql_connection_pool_max > 0.90
        for: 5m
        annotations:
          summary: "数据库连接池使用率过高: {{ $value }}"

十、面试要点

1️⃣ 高频追问

Q1: "如何保证短链码全局唯一?"

A: 三种方案:
1. 数据库自增ID(简单,但分布式扩展难)
2. 雪花算法(分布式友好,需处理时钟回拨)
3. Redis INCR(性能高,但依赖Redis持久化)

推荐:雪花算法,理由是分布式、高性能、趋势有序

Q2: "短链码长度如何设计?"

A: 根据业务规模:
- 6位Base62:支持568亿条(62^6)
- 7位Base62:支持3.5万亿条(62^7)
- 8位Base62:支持218万亿条(62^8)

考虑因素:
1. 短链越短,用户体验越好
2. 预估3-5年数据量
3. 留有扩展空间

推荐:7位Base62(支持大部分场景)

Q3: "如何处理缓存和数据库一致性?"

A: 采用Cache-Aside模式:
1. 读:先查缓存,未命中再查DB,并回写缓存
2. 写:先写DB,再删除缓存(而不是更新缓存)

为什么删除而不是更新?
- 更新缓存:可能出现脏数据(并发问题)
- 删除缓存:下次读取时重新加载(延迟双删)

延迟双删:
1. 删除缓存
2. 更新数据库
3. 延迟500ms后再次删除缓存(防止主从延迟)

Q4: "如何防止短链被恶意刷?"

A: 多层防护:
1. 限流:
   - 用户维度:每分钟最多生成10个短链
   - IP维度:每秒最多100次跳转
   - 使用令牌桶算法

2. 风控:
   - IP黑名单:识别恶意IP
   - 行为分析:检测异常访问模式
   - 验证码:疑似机器人时触发

3. CDN防护:
   - DDoS防护
   - CC攻击防护

Q5: "单机Redis内存不够怎么办?"

A: 三种方案:
1. Redis Cluster(官方方案)
   - 16384个槽位,哈希分片
   - 自动故障转移
   - 客户端智能路由

2. Twemproxy/Codis(代理分片)
   - 中间代理层
   - 对客户端透明
   - 支持在线扩容

3. 客户端分片
   - 应用层实现路由逻辑
   - hash(key) % node_count
   - 性能最高,但扩容困难

推荐:Redis Cluster(官方、稳定、成熟)

2️⃣ 加分项

主动提出扩展点:

如果时间充足,我们还可以讨论:
1. 自定义短链:冲突检测、敏感词过滤
2. 二维码生成:异步生成、CDN缓存
3. 多地域部署:GSLB、就近接入
4. 数据分析:BI报表、用户画像
5. A/B测试:多版本短链、流量分配

对比多个方案:

短链生成算法对比:
1. 自增ID:简单,但不适合分布式
2. 雪花算法:分布式友好,需处理时钟回拨
3. UUID:全局唯一,但太长(32位)
4. 哈希算法:无状态,但需处理冲突

综合考虑,推荐雪花算法,因为...

给出真实数据:

根据我在XX公司的经验:
- 短链跳转QPS可达5万/秒
- 缓存命中率稳定在98%以上
- P99响应时间< 20ms
- 每天处理1亿次跳转请求

Prev
系统设计方法论
Next
02 - 秒杀系统设计