11-系统设计面试方法论
本章导读
系统设计是高级工程师面试的核心环节,考察综合技术能力和架构思维。本章提供系统化的设计方法论,包含5+个完整案例(URL短链接、新闻推送、秒杀系统等),帮助读者掌握从需求澄清到技术选型的完整流程。
主要内容:
- 系统设计面试框架
- 需求澄清与范围界定
- 容量估算与性能指标
- 架构设计与技术选型
- 完整案例分析
第一部分:系统设计方法论框架
系统设计面试的四步法
Step 1: 需求澄清(5-10分钟)
目标:明确系统的功能需求和非功能需求
问题清单:
功能需求:
□ 核心功能有哪些?(如:短链接生成、跳转)
□ 需要支持哪些用户场景?
□ 需要哪些高级功能?(如:自定义短链、统计分析)
□ 是否需要用户系统?
□ 数据保留期限?
非功能需求:
□ 预期用户规模?(DAU、MAU)
□ 预期QPS?(读、写)
□ 响应时间要求?(P99延迟)
□ 可用性要求?(99.9%、99.99%)
□ 一致性要求?(强一致性、最终一致性)
□ 数据持久性要求?
约束条件:
□ 是否有遗留系统需要集成?
□ 是否有特定技术栈要求?
□ 预算限制?
Step 2: 容量估算(5-10分钟)
目标:评估系统规模,指导架构设计
估算维度:
流量估算:
- DAU(日活用户)
- 每用户日均请求数
- QPS = DAU × 日均请求 / 86400
- 峰值QPS = 平均QPS × 峰值系数(通常2-5倍)
存储估算:
- 单条记录大小
- 每日新增记录数
- 总存储 = 单条大小 × 记录数 × 保留天数
带宽估算:
- 写入带宽 = 写QPS × 单条大小
- 读取带宽 = 读QPS × 单条大小
示例(URL短链接服务):
假设:
- DAU = 1000万
- 每用户每天生成1个短链接
- 每用户每天点击10个短链接
流量估算:
- 写QPS = 1000万 / 86400 ≈ 116 QPS
- 读QPS = 1亿 / 86400 ≈ 1160 QPS
- 峰值读QPS = 1160 × 3 = 3480 QPS
存储估算:
- 单条记录:100字节(短链接+长链接+元数据)
- 日新增:1000万条
- 年存储:1000万 × 365 × 100B ≈ 365GB
- 5年存储:1.8TB
带宽估算:
- 写带宽:116 QPS × 100B ≈ 12KB/s
- 读带宽:1160 QPS × 100B ≈ 116KB/s
Step 3: 架构设计(20-30分钟)
目标:设计满足需求的系统架构
设计步骤:
1. 高层架构(High-Level Design)
- 画出主要组件(客户端、服务器、数据库、缓存等)
- 标注数据流向
2. 数据模型(Data Model)
- 设计数据库表结构
- 选择存储类型(SQL、NoSQL、文件存储)
3. API设计(API Design)
- 定义核心API接口
- 请求/响应格式
4. 核心算法(Core Algorithm)
- 关键业务逻辑的实现方案
5. 技术选型(Technology Stack)
- 数据库、缓存、消息队列等技术选择
- 说明选择理由
Step 4: 深入优化(10-15分钟)
目标:解决性能瓶颈、提高可靠性
优化方向:
性能优化:
□ 缓存策略(缓存什么、缓存多久、如何更新)
□ 数据库优化(索引、分库分表)
□ CDN加速
□ 异步处理
可靠性优化:
□ 容错设计(单点故障)
□ 数据备份
□ 限流降级
□ 监控告警
可扩展性优化:
□ 水平扩展方案
□ 负载均衡
□ 微服务拆分
安全性优化:
□ 认证授权
□ 数据加密
□ 防止滥用(限流、验证码)
第二部分:完整案例分析
案例1:设计URL短链接服务
场景: 类似bit.ly、tinyurl的短链接服务
Step 1: 需求澄清
功能需求:
核心功能:
1. 生成短链接:输入长URL,返回短URL
POST /api/shorten
Request: {"long_url": "https://www.example.com/very/long/path"}
Response: {"short_url": "https://short.ly/abc123"}
2. 跳转:访问短URL,重定向到长URL
GET /abc123 → 302 Redirect to long_url
高级功能(可选):
3. 自定义短链接:用户指定短码
4. 过期时间:设置短链接有效期
5. 统计分析:点击次数、来源、时间分布
非核心功能(暂不实现):
- 用户系统(匿名服务)
- 编辑/删除短链接
非功能需求:
规模:
- DAU: 1000万
- 日生成短链接:1000万个
- 日点击:1亿次
- 读写比:100:1
性能:
- 写延迟:< 200ms
- 读延迟:< 50ms
- 可用性:99.9%
一致性:
- 最终一致性(允许短暂不可用)
数据保留:
- 永久保留(或5年)
Step 2: 容量估算
流量估算:
写操作(生成短链接):
- QPS = 1000万 / 86400 ≈ 116 QPS
- 峰值QPS = 116 × 3 = 348 QPS
读操作(跳转):
- QPS = 1亿 / 86400 ≈ 1160 QPS
- 峰值QPS = 1160 × 3 = 3480 QPS
总结:读密集型系统
存储估算:
单条记录大小:
- 短码:7字符(62^7 ≈ 3.5万亿,足够使用)
- 长URL:平均2KB
- 元数据(创建时间、过期时间等):100B
- 总计:约2KB
存储需求:
- 日新增:1000万 × 2KB = 20GB
- 年存储:20GB × 365 = 7.3TB
- 5年存储:36.5TB
结论:需要分布式存储
带宽估算:
写带宽:
- 348 QPS × 2KB = 696KB/s ≈ 0.7MB/s
读带宽:
- 3480 QPS × 2KB = 6960KB/s ≈ 7MB/s
结论:带宽需求不高
Step 3: 架构设计
高层架构:
┌─────────────┐
│ 客户端 │
└──────┬──────┘
│
┌─────────────────────────────────────┐
│ 负载均衡器 (Nginx/ALB) │
└─────────────┬───────────────────────┘
│
┌───────┴───────┐
│ │
┌──────────┐ ┌──────────┐
│Web Server │ │Web Server │ (无状态,可水平扩展)
└─────┬─────┘ └────┬──────┘
│ │
└──────┬───────┘
│
┌────────┼────────┐
│ │ │
┌──────┐ ┌─────┐ ┌─────────┐
│ Redis │ │ DB │ │ ID生成器 │
│(Cache)│ │(主从)│ │ (Snowflake)│
└───────┘ └──────┘ └──────────┘
API设计:
1. 生成短链接
POST /api/shorten
Request:
{
"long_url": "https://www.example.com/path",
"custom_alias": "mylink", // 可选
"expire_at": "2024-12-31" // 可选
}
Response:
{
"short_url": "https://short.ly/abc123",
"long_url": "https://www.example.com/path",
"created_at": "2024-01-01T00:00:00Z"
}
错误处理:
- 400: 长URL格式错误
- 409: 自定义别名已存在
- 429: 请求过于频繁(限流)
2. 跳转
GET /:short_code
Response: 302 Redirect
Location: https://www.example.com/path
错误处理:
- 404: 短链接不存在
- 410: 短链接已过期
3. 统计(可选)
GET /api/stats/:short_code
Response:
{
"short_code": "abc123",
"clicks": 12345,
"created_at": "2024-01-01T00:00:00Z"
}
数据模型:
-- MySQL主库(写)
CREATE TABLE url_mappings (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
short_code VARCHAR(10) UNIQUE NOT NULL,
long_url VARCHAR(2048) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expire_at TIMESTAMP NULL,
user_id BIGINT NULL,
INDEX idx_short_code (short_code),
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB;
-- 统计表(写多读少,考虑单独存储)
CREATE TABLE url_stats (
short_code VARCHAR(10) PRIMARY KEY,
click_count BIGINT DEFAULT 0,
last_accessed_at TIMESTAMP,
INDEX idx_click_count (click_count)
) ENGINE=InnoDB;
-- 可选:访问日志表(用于详细分析)
CREATE TABLE access_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
short_code VARCHAR(10),
ip VARCHAR(45),
user_agent VARCHAR(255),
referer VARCHAR(255),
accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_short_code (short_code),
INDEX idx_accessed_at (accessed_at)
) ENGINE=InnoDB;
-- 数据分片策略(当单表过大时)
-- 方案1:按时间分表(按月)
url_mappings_202401
url_mappings_202402
...
-- 方案2:按short_code哈希分库分表
hash(short_code) % 16 → 选择分片
短码生成算法:
方案1:哈希 + Base62编码
算法:
1. 对长URL计算哈希(MD5、SHA256)
2. 取哈希值的前N位
3. Base62编码(0-9, a-z, A-Z)
伪代码:
function generateShortCode(longUrl) {
hash = md5(longUrl)
shortHash = hash.substring(0, 8) // 取前8位
shortCode = base62Encode(shortHash)
return shortCode // 返回7字符短码
}
优点:
- 相同URL生成相同短码(幂等性)
- 实现简单
缺点:
- 可能冲突(需要冲突检测和重试)
- 安全性低(可预测)
冲突处理:
function generateShortCode(longUrl) {
attempts = 0
while (attempts < 5) {
hash = md5(longUrl + attempts) // 加盐避免冲突
shortCode = base62Encode(hash.substring(0, 8))
if (!exists(shortCode)) {
return shortCode
}
attempts++
}
throw Error("Failed to generate unique short code")
}
---
方案2:分布式ID生成器(推荐)
使用Snowflake算法生成全局唯一ID,然后Base62编码
Snowflake ID结构(64位):
[1位符号] [41位时间戳] [10位机器ID] [12位序列号]
优点:
- 全局唯一,无冲突
- 趋势递增(便于数据库索引)
- 高性能(本地生成,无网络开销)
缺点:
- 需要维护机器ID
- 依赖时钟同步
伪代码:
function generateShortCode() {
id = snowflakeGenerator.nextId() // 如:1234567890123456789
shortCode = base62Encode(id) // 如:abc123d
return shortCode
}
实现(Go示例):
import "github.com/bwmarrin/snowflake"
// 初始化
node, _ := snowflake.NewNode(1) // 机器ID=1
// 生成ID
id := node.Generate()
shortCode := base62.Encode(id.Int64())
---
方案3:自增ID + Base62编码(简单场景)
使用数据库自增ID,Base62编码
优点:
- 实现简单
- 无冲突
缺点:
- 依赖数据库(性能瓶颈)
- 可预测(安全性低)
伪代码:
function generateShortCode(longUrl) {
id = db.insert(longUrl) // 插入数据库,返回自增ID
shortCode = base62Encode(id)
return shortCode
}
推荐方案:Snowflake + Base62
// Go实现
package main
import (
"github.com/bwmarrin/snowflake"
"github.com/jxskiss/base62"
)
var node *snowflake.Node
func init() {
var err error
node, err = snowflake.NewNode(1) // 机器ID从配置读取
if err != nil {
panic(err)
}
}
func generateShortCode() string {
id := node.Generate()
return base62.EncodeToString([]byte(id.String()))
}
// 使用
func createShortURL(longURL string) (string, error) {
shortCode := generateShortCode()
// 插入数据库
err := db.Create(&URLMapping{
ShortCode: shortCode,
LongURL: longURL,
}).Error
if err != nil {
return "", err
}
return "https://short.ly/" + shortCode, nil
}
核心流程:
生成短链接流程:
1. 客户端请求
POST /api/shorten
{"long_url": "https://example.com/path"}
2. Web Server处理
- 验证长URL格式
- 检查是否已存在(可选,查询缓存)
- 生成短码(Snowflake + Base62)
3. 写入数据库
INSERT INTO url_mappings (short_code, long_url)
VALUES ('abc123', 'https://example.com/path')
4. 写入缓存(可选,为后续读取加速)
Redis SET abc123 "https://example.com/path" EX 86400
5. 返回结果
{"short_url": "https://short.ly/abc123"}
时间复杂度:O(1)
跳转流程:
1. 客户端请求
GET /abc123
2. Web Server查询缓存
longUrl = Redis GET abc123
3. 缓存未命中,查询数据库
SELECT long_url FROM url_mappings WHERE short_code = 'abc123'
4. 写入缓存
Redis SET abc123 longUrl EX 86400
5. 更新统计(异步)
消息队列 <- {"short_code": "abc123", "ip": "1.2.3.4", ...}
6. 返回重定向
HTTP 302 Redirect
Location: https://example.com/path
时间复杂度:
- 缓存命中:O(1),延迟 < 10ms
- 缓存未命中:O(1)(数据库索引),延迟 < 50ms
Step 4: 深入优化
性能优化:
1. 缓存策略
# Redis缓存(热点数据)
key: short_code
value: long_url
TTL: 24小时
# 缓存更新策略:Cache-Aside
- 读:先查缓存,未命中再查数据库,回写缓存
- 写:先写数据库,再删除缓存(或更新缓存)
# 缓存预热(热点数据)
- 定期任务:查询TOP 10000热门短链接,预加载到缓存
- 启动时:加载热点数据
# 布隆过滤器(防止缓存穿透)
- 用途:快速判断短码是否存在
- 实现:Redis Bloom Filter或本地Bloom Filter
- 流程:
1. 查询Bloom Filter,如果不存在,直接返回404
2. 如果可能存在,查询缓存/数据库
---
2. 数据库优化
# 主从复制
Master: 写操作
Slave: 读操作(查询长URL)
# 读写分离
写请求 -> Master
读请求 -> Slave(负载均衡到多个从库)
# 分库分表(数据量大时)
方案:按short_code哈希分16个库
分片规则:
shard_id = hash(short_code) % 16
优点:
- 单库数据量降低
- 水平扩展
缺点:
- 跨分片查询复杂(但本系统基本不需要)
---
3. CDN加速
# 静态资源CDN
- 跳转页面(302页面)缓存到CDN
- 减少服务器压力
# 地理分布
- 边缘节点缓存热点短链接
- 降低跨地域延迟
---
4. 异步处理
# 统计更新异步化
流程:
1. 用户访问短链接
2. 立即返回重定向(不等待统计更新)
3. 发送消息到Kafka:{"short_code": "abc123", "ip": "1.2.3.4"}
4. 消费者异步更新统计
优点:
- 降低响应延迟
- 解耦统计逻辑
# 批量更新
- 消费者批量更新统计(如:每100条批量写入)
- 减少数据库压力
可靠性优化:
1. 容错设计
# 数据库高可用
- 主从复制(Master-Slave)
- 自动故障转移(MHA、Orchestrator)
- 多可用区部署
# Redis高可用
- Redis Sentinel(哨兵模式)
- Redis Cluster(集群模式)
# 服务高可用
- 多实例部署(无状态)
- 负载均衡(Nginx、ALB)
- 健康检查(探针)
---
2. 降级策略
# 场景1:数据库故障
- 降级:只读缓存,禁止生成新短链接
- 返回:503 Service Unavailable
# 场景2:Redis故障
- 降级:直接查询数据库(性能下降)
- 限流:保护数据库
# 场景3:统计服务故障
- 降级:停止统计更新,保证核心功能(跳转)
---
3. 限流
# 生成短链接限流
- 单IP限流:每分钟最多10个
- 全局限流:每秒最多500个(保护数据库)
# 跳转限流
- 单短码限流:防止恶意攻击(DDoS)
- 全局限流:每秒最多5000个
实现:
- Redis计数器 + 滑动窗口
- 令牌桶算法(Token Bucket)
---
4. 监控告警
监控指标:
- QPS(生成、跳转)
- 延迟(P50、P95、P99)
- 错误率(4xx、5xx)
- 数据库连接数
- 缓存命中率
告警规则:
- QPS突增(>阈值2倍)
- 延迟过高(P99 > 100ms)
- 错误率过高(> 1%)
- 缓存命中率过低(< 80%)
可扩展性优化:
1. 水平扩展
# Web Server
- 无状态设计,可任意增加实例
- 通过负载均衡器分发流量
# 数据库
- 主从复制,增加只读从库
- 分库分表,增加分片
# 缓存
- Redis Cluster,增加节点
---
2. 微服务拆分
服务划分:
- URL生成服务:负责生成短链接
- URL跳转服务:负责跳转
- 统计服务:负责统计分析
优点:
- 独立部署、扩展
- 故障隔离
缺点:
- 增加复杂度
安全性优化:
1. 防止滥用
# 限流
- 单IP限流:防止恶意生成大量短链接
- 验证码:频繁请求时要求验证
# 黑名单
- URL黑名单:禁止生成恶意网站的短链接
- IP黑名单:封禁恶意IP
---
2. 数据安全
# 敏感数据加密
- 长URL可能包含敏感信息(如:token)
- 考虑加密存储
# HTTPS
- 全站HTTPS,防止中间人攻击
---
3. 防止短码预测
# 使用随机生成算法
- Snowflake ID包含随机成分
- 避免使用连续自增ID(可预测)
案例2:设计新闻推送系统
场景: 类似今日头条、抖音的个性化内容推送系统
Step 1: 需求澄清
功能需求:
核心功能:
1. 发布内容:作者发布文章/视频
2. 关注/取消关注:用户关注作者
3. Feed流:用户查看关注作者的内容
4. 推荐:推荐用户可能感兴趣的内容
API设计:
1. 发布内容
POST /api/posts
{
"user_id": 123,
"content": "Hello World",
"media_urls": ["https://..."],
"tags": ["tech", "programming"]
}
2. 关注用户
POST /api/follow
{
"follower_id": 456,
"followee_id": 123
}
3. 获取Feed流
GET /api/feed?user_id=456&page=1&size=20
Response: [
{"post_id": 789, "user_id": 123, "content": "...", "created_at": "..."},
...
]
4. 获取推荐内容
GET /api/recommend?user_id=456&size=20
非功能需求:
规模:
- 用户数:1亿
- DAU:1000万
- 日发帖量:1000万
- 日Feed请求:1亿次
性能:
- Feed加载延迟:< 300ms
- 发帖延迟:< 500ms
一致性:
- 最终一致性(允许短暂延迟)
Step 2: 容量估算
流量估算:
发帖QPS:
- 平均QPS = 1000万 / 86400 ≈ 116 QPS
- 峰值QPS = 116 × 3 ≈ 350 QPS
Feed请求QPS:
- 平均QPS = 1亿 / 86400 ≈ 1160 QPS
- 峰值QPS = 1160 × 3 ≈ 3500 QPS
读写比:约 1000:1(读密集)
存储估算:
用户数据:
- 1亿用户 × 1KB/用户 = 100GB
帖子数据:
- 单条帖子:平均5KB(文本+元数据)
- 日新增:1000万 × 5KB = 50GB
- 年存储:50GB × 365 = 18TB
- 媒体文件(图片、视频):另外存储(对象存储)
关系数据:
- 平均每用户关注100人
- 1亿用户 × 100 × 8B = 80GB
Feed缓存:
- 活跃用户:1000万
- 每用户缓存20条Feed × 8B(post_id) = 160B
- 总计:1000万 × 160B = 1.6GB(可全部放入内存)
Step 3: 架构设计
高层架构:
┌────────────┐
│ 客户端 │
└──────┬─────┘
│
┌─────────────────────────┐
│ API Gateway │
└────┬────────────────┬───┘
│ │
┌──────────┐ ┌───────────┐
│Post Service│ │Feed Service│
└────┬───────┘ └─────┬──────┘
│ │
┌────┼─────────────────┼────┐
│ │ │ │
│ ┌─────┐ ┌────┐ ┌───┐ │
│ │ Posts│ │User│ │Feed│ │
│ │ DB │ │ DB │ │Cache│ │
│ └──────┘ └────┘ └────┘ │
│ │
│ ┌──────────────────┐ │
│ │ Message Queue │ │
│ │ (Kafka/RabbitMQ)│ │
│ └─────────┬────────┘ │
│ │ │
│ ┌───────────┐ │
│ │Fan-out │ │
│ │Service │ │
│ └────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │Recommendation │ │
│ │Service │ │
│ └──────────────────┘ │
└───────────────────────────┘
数据模型:
-- 用户表
CREATE TABLE users (
user_id BIGINT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_username (username)
) ENGINE=InnoDB;
-- 帖子表
CREATE TABLE posts (
post_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
content TEXT,
media_urls JSON,
tags JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB;
-- 关注关系表
CREATE TABLE follows (
follower_id BIGINT NOT NULL,
followee_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (follower_id, followee_id),
INDEX idx_follower (follower_id),
INDEX idx_followee (followee_id)
) ENGINE=InnoDB;
-- Feed表(可选,用于缓存预计算的Feed)
-- 实际实现中,Feed通常存储在Redis中
CREATE TABLE feeds (
user_id BIGINT NOT NULL,
post_id BIGINT NOT NULL,
created_at TIMESTAMP,
PRIMARY KEY (user_id, post_id),
INDEX idx_user_created (user_id, created_at DESC)
) ENGINE=InnoDB;
Feed生成策略:
方案1:拉模式(Pull)
原理:用户请求Feed时,实时查询关注作者的帖子
流程:
1. 用户请求Feed
2. 查询关注列表:SELECT followee_id FROM follows WHERE follower_id = ?
3. 查询帖子:SELECT * FROM posts WHERE user_id IN (followee_ids) ORDER BY created_at DESC LIMIT 20
4. 返回结果
优点:
- 实现简单
- 数据实时(无延迟)
- 节省存储(不需要预计算)
缺点:
- 查询慢(关注人数多时)
- 数据库压力大(每次请求都查询)
适用场景:
- 用户关注数少(< 100)
- 请求量小
方案2:推模式(Push)
原理:作者发帖时,推送到所有粉丝的Feed
流程:
1. 作者发布帖子
2. 查询粉丝列表:SELECT follower_id FROM follows WHERE followee_id = author_id
3. 写入每个粉丝的Feed:
FOR EACH follower:
Redis LPUSH feed:{follower_id} {post_id}
Redis LTRIM feed:{follower_id} 0 999 # 只保留最新1000条
4. 用户请求Feed:
Redis LRANGE feed:{user_id} 0 19 # 获取前20条
优点:
- 读取快(直接从缓存读取)
- 数据库压力小
缺点:
- 写入慢(大V发帖需推送给数百万粉丝)
- 存储开销大(每个用户都存储Feed)
- 不活跃用户浪费存储
适用场景:
- 大部分用户关注数适中(100-1000)
- 读多写少
方案3:推拉结合(Hybrid,推荐)
原理:
- 普通用户:推模式(Fan-out on Write)
- 大V用户:拉模式(Fan-out on Read)
流程:
1. 作者发布帖子
2. 判断作者类型:
- 普通用户(粉丝 < 10万):推送到所有粉丝Feed
- 大V用户(粉丝 >= 10万):只存储帖子,不推送
3. 用户请求Feed:
- 查询自己的Feed缓存(普通作者的帖子)
- 实时查询关注的大V的最新帖子
- 合并排序
优点:
- 平衡读写性能
- 节省存储
缺点:
- 实现复杂
伪代码:
function getFeed(userId) {
// 1. 从缓存获取普通作者的帖子
cachedPosts = Redis.LRANGE("feed:" + userId, 0, 19)
// 2. 查询关注的大V
bigVFollowees = DB.query("SELECT followee_id FROM follows WHERE follower_id = ? AND followee_type = 'bigv'", userId)
// 3. 查询大V的最新帖子
bigVPosts = DB.query("SELECT * FROM posts WHERE user_id IN (?) ORDER BY created_at DESC LIMIT 20", bigVFollowees)
// 4. 合并排序
allPosts = merge(cachedPosts, bigVPosts)
allPosts.sortByCreatedAtDesc()
return allPosts[0:19] // 返回前20条
}
推荐系统设计(简化版):
目标:推荐用户可能感兴趣的内容
方案:协同过滤 + 内容推荐
架构:
┌─────────────┐
│ User │
└──────┬──────┘
│
┌─────────────────────────┐
│ Recommendation Service │
└────┬────────────────┬───┘
│ │
┌────────┐ ┌───────────┐
│User │ │ Content │
│Profile │ │ Similarity │
│(Redis) │ │ (Vector DB)│
└─────────┘ └────────────┘
流程:
1. 用户画像(User Profile)
- 用户兴趣标签:["tech", "programming", "AI"]
- 历史行为:点赞、评论、浏览时长
2. 内容画像(Content Profile)
- 帖子标签:["tech", "tutorial"]
- 内容向量:[0.8, 0.2, 0.6, ...](通过NLP模型提取)
3. 召回(Recall)
- 基于标签:查询用户感兴趣标签的帖子
- 基于协同过滤:查询相似用户喜欢的帖子
- 基于热度:查询热门帖子
4. 排序(Ranking)
- 特征工程:用户特征、内容特征、交叉特征
- 排序模型:LR、GBDT、Deep Learning
- 输出:预测CTR(点击率)
5. 过滤(Filtering)
- 去重:已看过的帖子
- 时效性:过期的帖子
- 质量:低质量内容
6. 返回推荐结果
实现(简化):
function getRecommendations(userId, size) {
// 1. 获取用户画像
userProfile = Redis.get("user_profile:" + userId)
interests = userProfile.interests // ["tech", "AI"]
// 2. 召回候选集
candidates = []
// 2.1 基于标签召回
for tag in interests:
posts = DB.query("SELECT * FROM posts WHERE tags LIKE ? ORDER BY created_at DESC LIMIT 100", "%"+tag+"%")
candidates.addAll(posts)
// 2.2 基于协同过滤召回
similarUsers = getSimilarUsers(userId) // 找到相似用户
for similarUser in similarUsers:
posts = DB.query("SELECT * FROM posts WHERE post_id IN (SELECT post_id FROM user_interactions WHERE user_id = ? AND action = 'like')", similarUser)
candidates.addAll(posts)
// 2.3 基于热度召回
hotPosts = Redis.ZREVRANGE("hot_posts", 0, 99) // 热门帖子(按点赞数排序)
candidates.addAll(hotPosts)
// 3. 去重
candidates = deduplicate(candidates)
// 4. 排序(简化:按创建时间和热度综合排序)
candidates.sort((a, b) => {
scoreA = 0.7 * a.created_at + 0.3 * a.like_count
scoreB = 0.7 * b.created_at + 0.3 * b.like_count
return scoreB - scoreA
})
// 5. 返回TopN
return candidates[0:size-1]
}
Step 4: 深入优化
性能优化:
1. Feed缓存
# Redis存储结构
key: feed:{user_id}
value: List of post_id (按时间倒序)
示例:
feed:456 = [789, 788, 787, ...] # 用户456的Feed,最新帖子ID=789
# 缓存更新
- 写入:作者发帖时,推送到粉丝Feed
- 读取:用户请求Feed时,从缓存读取
- 过期:不设置TTL(永久有效,容量限制为1000条)
# 缓存预热
- 活跃用户Feed预加载
- 定时任务刷新
---
2. 数据库优化
# 分库分表
- 帖子表按post_id哈希分片
- 关注关系表按follower_id哈希分片
# 索引优化
- (user_id, created_at DESC)复合索引
- 覆盖索引避免回表
---
3. 异步处理
# Fan-out异步化
流程:
1. 作者发帖,写入数据库
2. 发送消息到Kafka:{"post_id": 789, "author_id": 123}
3. Fan-out服务消费消息,推送到粉丝Feed
4. 立即返回成功(不等待Fan-out完成)
优点:
- 发帖延迟低
- 解耦业务逻辑
---
4. CDN加速
# 媒体文件CDN
- 图片、视频上传到对象存储(S3)
- 通过CDN分发
# API缓存
- 热门内容API响应缓存到CDN
- 减少服务器压力
可靠性优化:
1. 降级策略
# 场景1:Fan-out服务故障
- 降级:暂停推送,Feed改为拉模式
- 恢复后:批量补推
# 场景2:推荐服务故障
- 降级:返回热门内容(预先缓存)
# 场景3:数据库故障
- 降级:只读缓存
---
2. 限流
# 发帖限流
- 单用户:每分钟最多10条
- 全局:每秒最多500条
# Feed请求限流
- 单用户:每秒最多5次
- 全局:每秒最多10000次
---
3. 监控
监控指标:
- Fan-out延迟(发帖到Feed可见的时间)
- Feed加载延迟
- 缓存命中率
- 消息队列堆积
告警:
- Fan-out延迟 > 5秒
- Feed加载延迟 > 500ms
- 缓存命中率 < 90%
- 消息队列堆积 > 10000
案例3:设计秒杀系统
场景: 电商秒杀(抢购)系统
Step 1: 需求澄清
功能需求:
核心功能:
1. 秒杀活动创建
2. 商品列表查询
3. 下单(秒杀)
4. 订单查询
关键特点:
- 高并发:百万级QPS
- 库存有限:1000件商品
- 超卖问题:必须精确控制库存
非功能需求:
规模:
- 参与用户:1000万
- 商品数量:1000件
- 秒杀时长:10秒
- 峰值QPS:100万
性能:
- 下单响应时间:< 500ms
- 成功率:库存范围内100%成功
一致性:
- 强一致性(不允许超卖)
Step 2: 容量估算
流量估算:
峰值QPS:
- 1000万用户在10秒内抢购
- QPS = 1000万 / 10 = 100万 QPS
成功率:
- 库存1000件,成功1000人
- 成功率 = 1000 / 1000万 = 0.01%
失败请求:
- 999.99%的请求失败(库存不足)
存储估算:
订单数据:
- 成功订单:1000条 × 1KB = 1MB(可忽略)
日志数据:
- 请求日志:1000万条 × 500B = 5GB
Step 3: 架构设计
高层架构:
┌────────────┐
│ 用户 │
└──────┬─────┘
│
┌─────────────────────────┐
│ CDN / API Gateway │
│ (限流、防刷) │
└────┬────────────────┬───┘
│ │
┌──────────┐ ┌───────────┐
│ 前置 │ │ 秒杀 │
│ 拦截 │ │ 服务 │
│ (Redis) │ └─────┬──────┘
└────────────┘ │
┌────┼────┐
│ │ │
┌─────┐ ┌─────┐ ┌──────┐
│Redis │ │MySQL │ │Message│
│(库存)│ │(订单)│ │Queue │
└──────┘ └──────┘ └───┬───┘
│
┌───────────┐
│ 订单处理 │
│ 服务 │
└────────────┘
核心流程:
秒杀流程:
1. 前置拦截(高性能)
目的:快速拦截无效请求
检查项:
- 用户是否登录(Token验证)
- 用户是否重复购买(Redis检查)
- 活动是否开始/结束(Redis检查)
- 库存是否充足(Redis检查)
伪代码:
function preCheck(userId, activityId) {
// 检查活动状态
活动 status = Redis.get("activity:" + activityId + ":status")
if (status != "ONGOING") {
return "活动未开始或已结束"
}
// 检查库存
stock = Redis.get("activity:" + activityId + ":stock")
if (stock <= 0) {
return "库存不足"
}
// 检查是否重复购买
purchased = Redis.sismember("activity:" + activityId + ":users", userId)
if (purchased) {
return "已购买过"
}
return "通过"
}
2. 库存扣减(原子操作)
方案1:Redis DECR
Lua脚本(保证原子性):
local stock = redis.call('get', KEYS[1])
if tonumber(stock) <= 0 then
return 0 -- 库存不足
end
redis.call('decr', KEYS[1])
redis.call('sadd', KEYS[2], ARGV[1]) -- 记录用户已购买
return 1 -- 成功
调用:
result = Redis.eval(luaScript,
["activity:123:stock", "activity:123:users"],
[userId]
)
if (result == 0) {
return "库存不足"
}
3. 创建订单(异步)
- 发送消息到Kafka:{"user_id": 456, "activity_id": 123}
- 立即返回成功
4. 订单处理(消费者)
- 消费Kafka消息
- 写入MySQL订单表
- 扣减MySQL库存(双写保证一致性)
- 发送短信/邮件通知
5. 兜底检查(防止超卖)
- 定时任务对比Redis库存和MySQL库存
- 如有差异,触发告警和补偿
API设计:
1. 秒杀请求
POST /api/seckill
Request:
{
"user_id": 456,
"activity_id": 123,
"token": "..." # 防刷Token
}
Response:
{
"success": true,
"order_id": "20240101-123456",
"message": "秒杀成功"
}
错误码:
- 1001: 活动未开始
- 1002: 活动已结束
- 1003: 库存不足
- 1004: 重复购买
- 1005: Token无效
2. 订单查询
GET /api/order/:order_id
数据模型:
-- 秒杀活动表
CREATE TABLE seckill_activities (
activity_id BIGINT PRIMARY KEY,
product_id BIGINT NOT NULL,
stock INT NOT NULL,
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP NOT NULL,
status VARCHAR(20), -- PENDING, ONGOING, FINISHED
INDEX idx_start_time (start_time)
) ENGINE=InnoDB;
-- 订单表
CREATE TABLE seckill_orders (
order_id VARCHAR(50) PRIMARY KEY,
user_id BIGINT NOT NULL,
activity_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
status VARCHAR(20), -- SUCCESS, CANCELLED
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_activity_id (activity_id)
) ENGINE=InnoDB;
-- Redis数据结构
# 活动状态
key: activity:{activity_id}:status
value: ONGOING / FINISHED
# 库存
key: activity:{activity_id}:stock
value: 1000
# 已购用户集合
key: activity:{activity_id}:users
value: Set<user_id>
# 防刷Token
key: token:{token}
value: user_id
TTL: 60秒
Step 4: 深入优化
性能优化:
1. 前端优化
# 防止重复点击
- 按钮置灰,倒计时
- 前端限流(1秒最多点击1次)
# 静态资源CDN
- 商品图片、CSS、JS缓存到CDN
---
2. 后端优化
# 多级缓存
- 浏览器缓存:静态资源
- CDN缓存:商品详情页
- Redis缓存:库存、活动状态
# 限流
- API网关限流:单IP每秒最多10次
- 服务端限流:令牌桶/漏桶算法
# 削峰
- 消息队列:订单处理异步化
- 流量控制:排队机制(如:虚拟队列)
---
3. 数据库优化
# 读写分离
- 查询操作:从库
- 写入操作:主库
# 连接池
- 预先建立连接池,避免频繁创建连接
---
4. 库存方案优化
方案1:Redis预扣 + MySQL最终扣减
优点:
- Redis性能高,支持高并发
- MySQL保证数据持久性
缺点:
- 两阶段操作,可能不一致
一致性保证:
- 定时对账(Redis库存 vs MySQL库存)
- 补偿机制(差异时回滚或补单)
---
方案2:Redis + Lua脚本(推荐)
Lua脚本:
local stock = redis.call('get', KEYS[1])
if tonumber(stock) <= 0 then
return 0
end
redis.call('decr', KEYS[1])
return 1
优点:
- 原子性(Lua脚本是单线程执行)
- 高性能
缺点:
- Redis单点故障风险(需要高可用方案)
---
方案3:数据库行锁(传统方案)
SQL:
SELECT stock FROM products WHERE product_id = 123 FOR UPDATE;
UPDATE products SET stock = stock - 1 WHERE product_id = 123;
优点:
- 严格一致性
缺点:
- 性能差(锁竞争严重)
- 不适合高并发
防止超卖:
1. 乐观锁(CAS)
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE product_id = 123
AND stock > 0
AND version = 5; -- 当前版本号
if (affectedRows == 0) {
return "库存不足或版本冲突"
}
优点:
- 无锁,性能好
缺点:
- 高并发时冲突率高,需要重试
---
2. 悲观锁(行锁)
BEGIN TRANSACTION;
SELECT stock FROM products WHERE product_id = 123 FOR UPDATE;
if (stock > 0) {
UPDATE products SET stock = stock - 1 WHERE product_id = 123;
COMMIT;
} else {
ROLLBACK;
return "库存不足"
}
优点:
- 绝对不会超卖
缺点:
- 锁竞争严重,性能差
---
3. Redis原子操作(推荐)
Redis DECR命令是原子的,天然避免超卖
stock = Redis.DECR("activity:123:stock")
if (stock < 0) {
Redis.INCR("activity:123:stock") # 回滚
return "库存不足"
}
优点:
- 原子性
- 高性能
缺点:
- Redis故障风险
防止恶意刷单:
1. 验证码
- 秒杀前弹出验证码
- 防止机器人
2. Token机制
- 秒杀前请求Token:GET /api/seckill/token?user_id=456&activity_id=123
- 服务端生成唯一Token,设置60秒过期
- 秒杀时携带Token,验证有效性
3. IP限流
- 单IP每秒最多10次请求
- 超过限制返回429 Too Many Requests
4. 用户行为分析
- 检测异常行为(如:秒杀前大量刷新页面)
- 封禁可疑账号
5. 风控系统
- 机器学习模型识别羊毛党
- 实时拦截可疑请求
高可用方案:
1. Redis高可用
- Redis Sentinel(哨兵)
- Redis Cluster(集群)
- 主从切换自动化
2. 数据库高可用
- 主从复制
- 自动故障转移(MHA)
3. 服务高可用
- 多实例部署
- 负载均衡
- 熔断降级(Hystrix)
4. 降级方案
- Redis故障:降级到数据库(限流保护)
- 数据库故障:停止秒杀,返回503
- 消息队列故障:同步创建订单(性能下降)