系统设计面试题
系统设计面试题
七、系统设计通用题
入门(补齐 20 题)
★ 系统设计先问什么? 先明确用户、核心功能、规模、延迟、可用性和数据约束,再画最小闭环。条件不足时把假设说出来,不直接堆技术名。
QPS 和并发是什么关系? 稳态下并发约等于吞吐量乘平均处理时长,注意单位。长连接数、模型运行数和数据库并发应分别估算。
★ 平均延迟和 P99 区别? 平均值会掩盖长尾,P99 表示约 99% 的请求不超过该值。按接口和版本观察,还要关注超时被统计口径漏掉。
吞吐量怎么估算? 从日活、每人操作数、时间分布估平均与峰值请求,再拆各下游放大量。场景数字应明确是假设,不当已有业务数据。
可用性指标怎么定义? 按成功请求比例或可用时间定义 SLI,并约定哪些错误算失败;SLO 是目标,不能只用进程存活代表业务可用。
★ 同步和异步如何选择? 用户需要立即结果的短操作可同步;耗时工作可先持久化意图返回 ID,再异步执行。异步会引入状态查询、重试与一致性成本。
横向扩容和纵向扩容区别? 前者加实例,后者给单实例加资源。横向扩容依赖状态与共享资源设计,数据库瓶颈不会因为多开 Web 自动消失。
有状态与无状态服务是什么? 无状态通常指请求不依赖某台实例的本地业务状态,不是整个系统没有数据。会话、文件和任务仍需可靠存储。
★ 缓存为什么不能当唯一真相? 可能过期、淘汰或丢失,应能从权威数据重建。严格计数/扣款不能只依赖普通缓存读改写。
读多写少如何优化? 先优化查询和索引,再按允许的新鲜度缓存、复制或预聚合。每加一层都要说明更新延迟与失效方式。
★ 超时为什么要分层? 连接、读取、总请求和业务执行是不同预算。给下游分配小于上游剩余时间的 deadline,避免上游已退出下游仍长期工作。
重试什么时候有害? 下游过载时同步重试会放大流量;写操作结果未知时还会重复副作用。只对可恢复错误有限重试,加退避、抖动和幂等。
★ 分页有哪些方式? offset 简单但深页成本大;游标按稳定排序键继续,适合连续浏览。两者都需唯一并列键与权限过滤。
消息顺序有什么范围? 通常只能在某个队列/分区/业务键范围约定顺序,不能泛称全系统有序。消费并发和失败重试也可能改变处理顺序。
数据分区解决什么? 按时间/租户等划分数据,改善维护或查询裁剪;不自动跨机器扩写,也不保证任意条件查询更快。
★ 主从复制等于备份吗? 不是,误删也可能复制到从库。备份应有独立保留点并验证恢复,明确恢复时间和可接受丢失量。
一致性读指什么? 要说明是线性一致、读己之写、单调读还是事务快照。不同需求对应不同代价,不能只回答「最终一致」。
★ 热点是什么? 少数键、用户、分区或资源承受大部分流量。按业务键观测,再做隔离、拆分、缓存与限流,平均 QPS 会掩盖热点。
为什么需要容量余量? 实例故障、发布重叠和突发流量会暂时增加负载。按最少可用实例承担峰值估算,不能把常态 CPU 用满当最高效率。
★ 如何评价设计的取舍? 逐项说明满足的约束、增加的复杂度、故障行为与验证方法。没有规模证据时优先可演进的简单方案。
中等
幂等怎么设计? 唯一请求 ID + 去重表 / Redis
SET NX;数据库唯一约束;状态机(只有「待支付」才能变「已支付」:UPDATE ... WHERE status = 'pending');乐观锁版本号。分布式 ID 怎么生成? UUID(简单,无序)、UUID v7(有序)、雪花算法(时间戳 + 机器号 + 序列号,64 位整数、有序,注意时钟回拨)、数据库号段模式(一次取一段 ID 缓存在本地)、Redis
INCR。限流算法? 固定窗口、滑动窗口、令牌桶、漏桶(见 08 文档第九节)。
熔断、降级、限流的区别? 限流:控制进来的流量;熔断:下游故障时暂时不调用它(快速失败);降级:保核心功能、关掉非核心功能或返回兜底结果。我们的模型层三者都有:限流器、熔断器、兜底模型链。
负载均衡算法? 轮询、加权轮询、最少连接、IP 哈希、一致性哈希(节点增减时只影响一小部分 key 的映射,用于缓存、粘性会话)。我们的 HAProxy 按用户 ID 一致性哈希,让同一个用户尽量落到同一个副本。
CAP 和 BASE? CAP:分区容错(P)必须有,在一致性(C)和可用性(A)之间取舍。CP:ZooKeeper、etcd;AP:Eureka、Cassandra。BASE:基本可用、软状态、最终一致,是对 AP 的工程化描述。
缓存一致性? 先更新数据库再删缓存 + 过期兜底 + binlog 订阅删缓存(见 08 文档第七节)。
消息队列的作用? 解耦、异步、削峰。代价:系统复杂度、一致性问题、消息丢失 / 重复 / 乱序要处理。
★ 全局限流如何实现? 统一入口或共享原子计数器按用户/组织/接口分桶;定义突发额度、时间窗口与依赖故障时的策略,不能把每实例限流相加当固定总额。
租户公平调度怎么做? 为每租户限制在途数和排队配额,大批量任务分批入队;看最老等待时长,避免大客户占满消费者。
为什么锁要有 owner token? 解锁必须确认当前持有者,防旧请求误删新锁;长任务还需续租和 fencing,TTL 到期不意味着旧执行已经停止。
★ Outbox 如何领取任务? 短事务写领取 token/租约并提交,在事务外发消息,再按 token 确认;过期可回收。重复发送由消费者幂等处理。
读写分离怎样保证读己之写? 写后相关读取暂时走主库,或用复制位点等待等机制。固定睡眠只降低概率,不保证从库已经追上。
★ 游标包含哪些字段? 包含排序值、唯一 ID 和必要版本,绑定筛选条件;验证格式并防篡改。数据变化的分页语义需要明确。
消息积压看哪些指标? ready 数、最老消息年龄、在途数、吞吐和失败重试;数量不大但每条执行很久也可能等待超标。
配置中心如何安全变更? 配置有 schema、版本、灰度和回退;进程加载形成一致快照,避免一次运行里模型与权限分别读到不同版本。
★ 多层重试如何避免叠乘? 总 deadline 与重试预算下传,明确由哪一层负责;将尝试次数和原因写日志,防 HTTP×工具×模型三层各重试三次。
缓存键怎么设计? 包含业务输入、租户/权限范围与数据版本,规范化编码防冲突;过期和容量有上限,不能只按 URL 或自然语言问题做私有结果缓存。
批量接口怎样避免放大压力? 限制条目数、并发与每条成本,独立返回失败或整体原子语义;不能把 1 个 HTTP 请求误当成只有 1 次数据库操作。
★ 灰度应该观察什么? 按版本比较成功率、延迟、成本与关键业务结果,设置回退阈值;总体平均可能掩盖小流量新版本失败。
高难
分布式事务有哪些方案?
方案 原理 适合 2PC / XA 协调者先让所有参与者准备,都成功再统一提交 强一致,但阻塞、性能差,协调者单点 TCC 业务层实现 Try(预留)/ Confirm(确认)/ Cancel(取消) 强一致要求高、能改业务代码,比如支付 Saga 拆成一串本地事务,失败时按反向顺序执行补偿 长流程(订单 → 库存 → 物流) 本地消息表 / Outbox 业务写库时在同一个本地事务里写一条「待发送消息」,后台任务可靠投递,失败重试 最常用,最终一致 事务消息 RocketMQ 的半消息机制 用 RocketMQ 的场景 项目真实做法:通知用 outbox 模式。业务事务里登记一条投递记录,后台 worker 投递到 Redis 通知 + SSE;行 ID 就是修订号,写 Redis 时用 Lua 原子比较修订号,旧修订丢弃、同修订不重复覆盖。解决了「业务提交了但通知没发出去」和「重复 / 乱序投递」两个问题。 单体 vs 微服务? 微服务的好处是独立部署、独立扩容、技术异构、故障隔离;代价是分布式复杂度(调用链、一致性、运维)。团队小、业务没拆清楚时单体更合适。我们是「一份代码、按进程角色拆成三个服务」(
APP_SCOPE):共享代码和模型,独立部署和扩容,console / gateway 进程不加载 AI 重依赖。这是单体和微服务之间的一个务实折中。可观测性三件套? 日志(发生了什么)、指标(Prometheus:QPS、延迟、错误率、资源)、链路追踪(OpenTelemetry / Jaeger:一个请求经过了哪些服务、每段多久)。Agent 系统额外要有:每一轮的模型输入输出、token、缓存命中、工具调用记录(我们的 run_log 和请求账本)。
怎么设计一个高可用的系统? 消除单点(多副本、主从);故障隔离(不同业务分队列、分资源池);超时和重试(带退避、有上限);限流熔断降级;可观测和告警;容量规划和压测;灰度发布和快速回滚。
★ 多地域如何选一致性? 先按业务拆哪些必须串行、哪些可异步复制,明确分区时拒绝还是接受写。不要同时承诺跨地域低延迟与所有写强一致却不说明代价。
跨服务「恰好一次」怎么实现? 把目标收窄为业务效果一次,靠原子幂等提交与外部幂等键;消息可能重复。外部结果未知时需要查询/对账,不能只加消费去重表。
★ 数据库提交结果未知怎么处理? 连接断开不证明提交失败,用稳定操作 ID 重查权威状态,再决定继续或补偿。盲目重试会重复,盲目删除产物也可能破坏已提交结果。
分布式租约怎么防旧持有者写入? 新领取生成递增版本或 fencing token,写入接收端拒绝旧 token。仅有锁 TTL 和续租线程不能阻止暂停后恢复的旧进程。
★ 大客户突发影响全部用户怎么改? 按租户限并发/队列,隔离大客户资源并做背压;分别测吞吐和等待公平性。只扩 worker 可能把数据库或模型服务打爆。
扩容后吞吐不涨为什么? 看共享数据库、锁、连接池、外部配额、网络和 CPU;画每段排队时间。若瓶颈未变,多实例只增加争用与成本。
时钟回拨影响哪些机制? 影响 ID、过期、调度和业务时间比较。测时长用单调钟,跨系统状态尽量基于同一权威时间源;墙钟时间不能承担全部互斥语义。
★ 缓存击穿怎样避免? 对热点回源合并请求、设置有限锁与旧值降级,保护数据库;持锁者失败后可恢复。敏感权限不能为了可用性继续返回撤权前旧值。
大规模数据回填如何不停业务? 分批按稳定键推进、保存水位并幂等,限速观察锁与复制延迟;增量更新有版本比较,避免回填旧值覆盖新写。
★ 补偿一定能恢复原样吗? 不一定,邮件无法撤回、外部价格可能变化。Saga 补偿是业务动作而非时间倒流;明确不可逆步骤和人工处理路径。
全链路 deadline 如何分配? 入口给总预算,各下游使用剩余预算扣除处理余量;取消要向下游传播。请求结束后仍运行的工作要显式转可靠后台或清理。
★ 异步状态如何避免倒退? 状态更新携带业务版本/序号,条件更新拒绝旧事件,终态与重开运行分版本。只按消息到达时间排序不能解决延迟和重投。
热分区怎么迁移? 先定位键分布与增长,设计双读/双写或路由版本,回填校验后切换并保留旧映射。迁移期间幂等与顺序语义要保持。
★ 怎样证明恢复预案可用? 在隔离环境从备份恢复并重放增量,验证数据、权限和代表性业务;实测 RPO/RTO。只有备份文件存在不能证明可恢复。
多层缓存如何排错? 给结果带数据版本/命中层信息,逐层绕过定位;区分浏览器、代理、应用与数据库缓存。不要一遇旧数据就清空全部系统缓存。
★ 如何做故障预算? 用 SLO 允许的失败量指导发布和可靠性投入,预算消耗快时先处理主因。预算不是允许忽略严重数据泄露或账务错误的理由。
