Skip to content

系统设计题 — 知识详解

更新: 5/15/2026 字数: 0 字 时长: 0 分钟

1. 系统设计方法论

面试中的系统设计题,按以下步骤回答:

  1. 需求分析:明确功能需求和非功能需求(性能、可靠性、扩展性)
  2. 模块拆分:划分核心模块,明确职责
  3. 接口设计:定义模块间的 API
  4. 数据流:数据从输入到输出的完整流转
  5. 异常处理:降级、重试、兜底策略
  6. 扩展性:如何支持未来需求变化

2. 设计一个图片加载框架

2.1 需求分析

  • 从网络/本地加载图片到 ImageView
  • 三级缓存(内存/磁盘/网络)
  • 生命周期感知(页面销毁取消加载)
  • 图片变换(圆角、裁剪、模糊)
  • 占位图和错误图
  • 采样压缩,避免 OOM

2.2 架构设计

ImageLoader(入口,Builder 模式配置)
├── RequestManager(生命周期管理)
├── Engine(调度核心)
│   ├── MemoryCache(LruCache)
│   ├── DiskCache(DiskLruCache)
│   ├── Fetcher(网络/本地/资源加载器)
│   └── Decoder(BitmapFactory 解码 + 采样)
├── TransformationPipeline(图片变换链)
└── Target(ImageView 绑定 + 占位图管理)

2.3 核心流程

load(url).into(imageView)
  → 生成缓存 key(url + 尺寸 + 变换参数 的 hash)
  → 查内存缓存 → 命中则直接显示
  → 查磁盘缓存 → 命中则解码 + 写入内存缓存 + 显示
  → 网络请求 → 下载 → 写入磁盘缓存 → 解码 → 变换 → 写入内存缓存 → 显示

2.4 关键设计点

生命周期管理

  • 注入空 Fragment(类似 Glide 的 RequestManagerFragment)监听生命周期
  • onStop 暂停加载,onStart 恢复,onDestroy 取消并释放资源

线程调度

  • 网络请求和磁盘 IO 在后台线程池
  • 图片解码在 CPU 密集型线程池
  • 显示切回主线程

防止图片错位

  • ImageView 设置 tag 为当前请求的 url
  • 加载完成后检查 tag 是否匹配,不匹配则丢弃

采样压缩

  • inJustDecodeBounds=true 获取原始尺寸
  • 根据目标 ImageView 尺寸计算 inSampleSize
  • 再解码,避免加载超大图片 OOM

3. 设计一个消息推送系统

3.1 需求分析

  • 服务端实时推送消息到客户端
  • 消息可靠性(不丢失、不重复)
  • 离线消息
  • 省电省流量

3.2 架构设计

服务端
├── 推送网关(维护长连接)
├── 消息队列(Kafka/RabbitMQ)
└── 离线消息存储

客户端
├── 连接管理(长连接 + 心跳 + 重连)
├── 消息接收与 ACK
├── 本地消息存储
└── 通知展示

3.3 长连接方案

  • WebSocket:基于 TCP,全双工通信,适合实时推送
  • 自定义 TCP 协议:更灵活,可以优化协议头大小
  • MQTT:轻量级发布/订阅协议,适合 IoT 和移动端

3.4 心跳机制

客户端每 N 秒发送心跳包 → 服务端回复 ACK
连续 M 次无 ACK → 判定连接断开 → 触发重连

心跳间隔自适应:

  • WiFi 环境:较长间隔(如 5 分钟)
  • 移动网络:较短间隔(如 2 分钟)
  • NAT 超时前发送心跳保活

3.5 消息可靠性

服务端发送消息(带 msgId)
  → 客户端收到 → 发送 ACK(带 msgId)
  → 服务端收到 ACK → 标记已送达
  → 超时未收到 ACK → 重发

客户端去重:本地维护已处理的 msgId 集合

3.6 离线消息

客户端上线后,拉取离线消息:

  • 客户端发送最后收到的 msgId
  • 服务端返回该 msgId 之后的所有消息
  • 分页拉取,避免一次返回过多

4. 设计一个路由框架

4.1 需求分析

  • 通过 URL/path 跳转到目标页面
  • 支持参数传递
  • 拦截器(登录检查、权限验证)
  • 降级策略(目标页面不存在时的处理)

4.2 架构设计

Router
├── RouteTable(path → Class 映射表)
├── Interceptor Chain(拦截器链)
├── Navigator(实际跳转执行)
└── DegradeHandler(降级处理)

4.3 路由表生成

编译期方案(推荐):

  • 自定义注解 @Route(path = "/user/detail")
  • APT 扫描注解,为每个模块生成路由表
  • 应用启动时加载所有模块的路由表

运行时方案

  • 手动注册:Router.register("/user/detail", UserDetailActivity::class)
  • 缺点:容易遗漏,不够自动化

4.4 拦截器

kotlin
interface RouteInterceptor {
    fun intercept(chain: RouteChain): RouteResult
}

class LoginInterceptor : RouteInterceptor {
    override fun intercept(chain: RouteChain): RouteResult {
        if (needLogin(chain.route) && !isLoggedIn()) {
            return RouteResult.Redirect("/login") // 重定向到登录页
        }
        return chain.proceed() // 继续
    }
}

4.5 降级策略

  • 目标页面未注册 → 打开 H5 兜底页
  • 目标页面崩溃 → 打开错误页
  • 服务端下发路由配置,动态控制跳转目标

5. 设计一个埋点系统

5.1 需求分析

  • 采集用户行为数据(页面浏览、点击、曝光)
  • 高性能,不影响主线程
  • 数据可靠,不丢失
  • 支持实时和批量上报

5.2 架构设计

采集层
├── 页面埋点(自动化:Lifecycle 监听)
├── 点击埋点(AOP / 注解)
├── 曝光埋点(RecyclerView 可见性检测)
└── 自定义埋点(手动调用)

处理层
├── 数据格式化(统一 schema)
├── 数据聚合(合并相同事件)
└── 本地缓存(SQLite / 文件)

上报层
├── 实时上报(关键事件立即发送)
├── 批量上报(定时 + 条数阈值)
└── 降级策略(网络异常时本地缓存,恢复后补报)

5.3 关键设计点

不影响主线程

  • 采集在主线程(获取 UI 信息),但只做最小操作
  • 格式化、缓存、上报在后台线程

数据可靠性

  • 先写本地缓存,上报成功后删除
  • 应用崩溃时数据不丢失(已写入本地)
  • 上报失败自动重试

曝光埋点

  • 监听 RecyclerView 的滚动事件
  • 计算 item 可见面积比例(>50% 且停留 >500ms 算有效曝光)
  • 去重:同一个 item 在同一页面只上报一次

6. 设计一个日志系统

6.1 架构

Logger API(统一接口)
├── 日志分级(VERBOSE/DEBUG/INFO/WARN/ERROR)
├── 日志格式化(时间/线程/TAG/内容)
├── 输出策略
│   ├── Console(Logcat,Debug 模式)
│   ├── File(本地文件,Release 模式)
│   └── Remote(上报服务端,ERROR 级别)
└── 文件管理
    ├── 按天/大小分文件
    ├── mmap 写入(高性能)
    ├── 压缩(gzip)
    └── 定期清理(保留 7 天)

6.2 高性能写入

使用 mmap 内存映射写入日志文件:

  • 写入内存即完成,OS 异步刷盘
  • 即使应用崩溃,已写入的数据不会丢失(OS 会刷盘)
  • 比 FileOutputStream 快 10 倍以上

美团 Logan、微信 xlog 都采用 mmap 方案。


7. 设计一个网络层框架

7.1 架构

NetworkClient(统一入口)
├── 拦截器链
│   ├── 日志拦截器
│   ├── 认证拦截器(Token 管理 + 自动刷新)
│   ├── 缓存拦截器
│   ├── 重试拦截器(指数退避)
│   └── 监控拦截器(耗时/成功率/错误码统计)
├── 请求队列(优先级调度)
├── 连接管理(连接池复用)
└── 数据解析(JSON/Protobuf)

7.2 Token 自动刷新

kotlin
class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor {
    override fun intercept(chain: Chain): Response {
        val request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer ${tokenManager.accessToken}")
            .build()

        val response = chain.proceed(request)

        if (response.code == 401) {
            synchronized(this) {
                // 双重检查:可能其他线程已经刷新了
                val newToken = tokenManager.refreshToken()
                val newRequest = request.newBuilder()
                    .header("Authorization", "Bearer $newToken")
                    .build()
                return chain.proceed(newRequest)
            }
        }
        return response
    }
}

7.3 重试策略

指数退避:第 1 次重试等 1 秒,第 2 次等 2 秒,第 3 次等 4 秒...加上随机抖动避免惊群效应。

只对幂等请求重试(GET),非幂等请求(POST)需要业务层决定。