HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 技术面试完全指南

    • 技术面试完全指南
    • 8年面试官告诉你:90%的简历在第一轮就被刷掉了
    • 刷了500道LeetCode,终于明白大厂算法面试到底考什么
    • 高频算法题精讲-双指针与滑动窗口
    • 03-高频算法题精讲-二分查找与排序
    • 04-高频算法题精讲-树与递归
    • 05-高频算法题精讲-图与拓扑排序
    • 06-高频算法题精讲-动态规划
    • Go面试必问:一道GMP问题,干掉90%的候选人
    • 08-数据库面试高频题
    • 09-分布式系统面试题
    • 10-Kubernetes与云原生面试题
    • 11-系统设计面试方法论
    • 前端面试高频题
    • AI 与机器学习面试题
    • 行为面试与软技能

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
   - 消息队列故障:同步创建订单(性能下降)

Prev
10-Kubernetes与云原生面试题
Next
前端面试高频题