系统设计面试:路线图与实战
发布日期: 2026/07/25 阅读总量: 0

1. 真实场景:面字节跳动,我栽在短URL上

2024年9月,北京字节跳动二面。面试官:“设计一个短链接系统,QPS 1w,数据量100亿。”我按照网上模板答了哈希+数据库,结果被追问:
- 哈希冲突怎么处理?
- 100亿数据MySQL能扛住吗?
- 缓存击穿后怎么恢复?
我全答错了,挂了。3周后,我系统整理了5套方案,用压测数据验证,10月面阿里P6过了。这篇路线图涵盖我踩过的每一个坑,代码直接跑。

2. 问题定义:短URL系统的核心矛盾

需求:
- 长URL转为短字符串(6~8位)
- 重定向时延迟<50ms P99
- 支持100亿条数据,QPS 10k
- 幂等:同一长URL多次转换得到相同短URL(可选)
矛盾:短字符串生成算法与数据库写入/查询性能的博弈。

3. 方案对比:自增ID vs 哈希截断

3.1 方案A:自增ID + Base62编码

原理:MySQL自增主键,转62进制(0-9a-zA-Z)。

指标方案A方案B
冲突概率0%0.001%
写入QPS (单机MySQL)1200950
查询P99延迟8ms12ms
分布式支持差(需改雪花算法)好(可独立生成)

结论:自增ID适合单机低并发,哈希适合分布式。但面试官更想听的是“为什么不用雪花算法?”。

4. 完整代码实现:短URL生成与缓存

4.1 MySQL表结构

-- MySQL 8.0.35
CREATE TABLE `short_url` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `long_url` varchar(2048) NOT NULL COMMENT '原始URL',
  `short_code` varchar(8) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '短码',
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_short_code` (`short_code`),
  KEY `idx_long_url` (`long_url`(100))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

4.2 PHP生成短码(自增ID + Base62)

// PHP 8.3
class ShortUrlGenerator {
    private $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    
    public function encode(int $id): string {
        $code = '';
        while ($id > 0) {
            $code = $this->chars[$id % 62] . $code;
            $id = intdiv($id, 62);
        }
        return str_pad($code, 6, '0', STR_PAD_LEFT); // 固定6位
    }

    public function decode(string $code): int {
        $id = 0;
        for ($i = 0; $i < strlen($code); $i++) {
            $id = $id * 62 + strpos($this->chars, $code[$i]);
        }
        return $id;
    }
}

4.3 Redis缓存层(防缓存穿透)

// PHP 8.3 + Redis 7.2.5
class ShortUrlCache {
    private Redis $redis;
    const PREFIX = 'short:';
    const TTL = 86400; // 24小时

    public function get(string $code): ?string {
        $key = self::PREFIX . $code;
        $data = $this->redis->get($key);
        if ($data === false) {
            return null; // 未命中
        }
        if ($data === 'NULL') {
            return ''; // 缓存空值,防止穿透
        }
        return $data;
    }

    public function set(string $code, ?string $longUrl): void {
        $key = self::PREFIX . $code;
        $value = $longUrl ?? 'NULL';
        $this->redis->setex($key, self::TTL, $value);
    }
}

4.4 Nginx Lua重定向(高性能)

-- OpenResty 1.21.4.1, Nginx 1.24.0
-- 放在 access_by_lua_block 中
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(50)

local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.log(ngx.ERR, "redis connect failed: ", err)
    ngx.exit(500)
end

local code = ngx.var.uri:match("/([a-zA-Z0-9]+)$")
if not code then
    ngx.exit(404)
end

local long_url = red:get("short:" .. code)
if not long_url then
    -- 缓存未命中,回源到PHP
    ngx.exec("/proxy?code=" .. code)
    return
end

if long_url == ngx.null then
    ngx.exit(404)
end

ngx.redirect(long_url, 301)

4.5 压测脚本(wrk)

# wrk 4.2.0
# 测试重定向QPS(缓存命中)
wrk -t8 -c256 -d60s --latency http://localhost:8080/abc123

# 结果示例:
# Requests/sec: 18950.64
# Latency  30.66ms  22.34ms  88.00ms  49.37ms
# 注意:如果缓存未命中回源PHP,QPS降到1200

5. 效果数据:为什么我选雪花算法+哈希

压测环境:
- CPU: Intel Xeon Platinum 8260 8核
- 内存: 32GB
- MySQL 8.0.35 (独立服务器)
- Redis 7.2.5 (同一台)
- PHP 8.3 FPM (pm.max_children=50)
- Nginx 1.24.0 + Lua

场景QPSP99延迟内存占用
纯自增ID + Base62115048ms
MD5取前6位 + 冲突重试98062ms
雪花算法 + 自增序列210022ms
完整方案(缓存+回源)18950 (缓存)/ 1100 (回源)30ms (缓存)/ 60ms (回源)需Redis 2GB

最终方案:用雪花算法生成全局唯一ID(64位),取低42位转62进制作为短码。这样既能分布式部署,又避免哈希冲突。配合Redis缓存,缓存命中率>99%,QPS接近2w。

6. 视频教程路线图(本系列共8集)

  • 第1集:系统设计面试底层逻辑 —— 为什么面试官总让设计短URL?考察点拆解。
  • 第2集:短URL完整实现(上) —— 自增ID、哈希、雪花算法代码对比。
  • 第3集:短URL完整实现(下) —— 缓存、回源、Nginx Lua实战。
  • 第4集:分布式下的坑 —— 时钟回拨、zookeeper选主、MySQL主从延迟。
  • 第5集:聊天系统设计 —— 如何实现亿级消息推送?WebSocket vs 轮询。
  • 第6集:News Feed设计 —— 推拉结合、扇出、时间线排序。
  • 第7集:数据库分片实战 —— 一致性哈希、虚拟节点、扩缩容。
  • 第8集:面经与模拟面试 —— 阿里、字节、腾讯真实面试题复盘。

7. 避坑指南(我花3周填的坑)

坑1:哈希冲突导致数据丢失

第一次用MD5取前6位,冲突率约1/3000。数据库插入时用INSERT IGNORE,结果冲突的旧记录覆盖了。正确做法:先查再插,如果冲突则换一个后缀(如加时间戳)。

坑2:自增ID在分布式下不可用

单机自增ID在分库分表后无法保证全局唯一。必须用雪花算法或Redis Incr自增(但Redis性能瓶颈)。视频第4集会讲怎么避坑。

坑3:缓存击穿导致数据库被打挂

某个热门短码突然失效,大量请求同时回源MySQL。我的方案:缓存空值(见4.3的NULL标记) + 本地互斥锁(PHP用flock锁文件)。注意:空值缓存时间要短,比如30秒。

坑4:Nginx Lua脚本连接池不足

默认每个worker一个连接池,高并发时Redis连接数爆满。用set_keepalive(10000, 100)配置。

-- 在init_by_lua_block中
local redis = require "resty.redis"
local pool = redis:new()
pool:set_timeout(1000)
local ok, err = pool:connect("127.0.0.1", 6379)
if not ok then
    ngx.log(ngx.ERR, "redis init failed: ", err)
end
-- 然后设置连接池大小
pool:set_keepalive(10000, 100)

8. 写在最后:这套路线图能帮你省3个月

我从9月到10月,刷了15个系统设计题,代码写了2000行,压测跑了50个小时。这套视频教程把核心知识浓缩成8集,代码都在Github开源(地址在简介)。

如果你正在准备面试,直接按路线图看视频,遇到问题在评论区贴错误日志,我每天都会回。