面试被问"设计一个短链/秒杀/朋友圈",或者工作里要把一个单体拆成能扛百万 QPS 的系统——靠的就是系统设计能力。本页给你一套可复用的方法论 + 一组核心积木:需求估算、一致性与可用性权衡、缓存/CDN/负载均衡、数据库扩展、消息队列、高可用三板斧,最后用三个真实案例把积木拼起来。学完你不会背答案,但能对着任何需求搭出靠谱骨架,并说清每一处的 trade-off。基线:云原生、容器化、托管中间件已成默认选项。
需求澄清与容量估算:别一上来画架构图
系统设计最忌"闷头画架构图"。正确顺序是先澄清约束、做量级估算,再动手。量级估算决定了你要不要分库、要不要加缓存、要不要上队列——它是区分"背过答案"和"真会设计"的关键。
先澄清需求:功能 vs 非功能
很多人拿到题直接写"用 Redis、用 Kafka"。停下。先问清楚两类需求:
- 功能性需求:系统要做什么(发短链、刷朋友圈、下单)。
- 非功能性需求:多快(延迟)、多稳(SLA)、多大(数据量/峰值 QPS)、一致到什么程度。
"做个短链"听起来简单,但每秒 10 次和每秒 10 万次是两套架构。不问清峰值、数据保留期、延迟要求,设计出来的东西要么过度工程、要么上线即崩。先问,再画。
容量估算:从用户数算 QPS
你不用算到精确,但要能估算量级。经典套路:从"日活 × 人均操作数 ÷ 86400"得到平均 QPS,再乘峰值系数(通常 2~5)得到峰值 QPS。
论估算的意义不在数字,在"走向"
算出来的 QPS 可能差一倍,但结论稳:读多写少就猛加缓存、读写均衡就要分片、峰值高就要异步削峰。量级判断错了,架构方向就错;具体数字差一点,后面都能靠伸缩补。
存储估算:数据量与增长
存储决定"单机存不存得下""要不要分库分表"。按"单条大小 × 条数 × 保留期"估。
读写比与热点识别
不是所有数据均匀访问。二八定律:20% 的内容贡献 80% 的流量。识别出热点(大 V、爆款),才能针对性做本地缓存、多副本。
| 读写特征 | 架构重点 |
|---|---|
| 读多写少(资讯、商品) | 缓存为主、读写分离 |
| 写多读少(日志、计数) | 异步落库、批量写、列存 |
| 强一致写(交易、库存) | 主库直写、分布式事务 |
| 有明显热点 | 本地缓存 + 多副本 + 防击穿 |
SLA 与可用性目标:几个"9"
可用性常用"几个 9"表达,它直接决定你要做多少冗余。每少一个 9,允许的宕机时间指数级收紧。
论可用性是被"冗余 + 自动切换"买来的
提高可用性 = 消灭单点 + 故障时自动切换。但每加一层冗余都加成本和复杂度。先问业务要几个 9,再决定投入。一个内部后台要 99.999% 是过度设计,一个支付网关要 99.9% 是拿命省钱。
定义核心接口与数据模型
估算完,先把"系统对外长什么样"钉死:核心 API、关键表/字段。这步能逼你想清边界,也方便后面逐层拆解。
把估算变成架构决策
估算不是写给自己看的,它要直接驱动选型。把数字翻译成"要不要上某块积木",才算用对了估算。
| 估算结论 | 对应的架构决策 |
|---|---|
| 峰值读 QPS 高 | 上 CDN + Redis 缓存,主从读写分离 |
| 峰值写 QPS 高 | 消息队列异步削峰,分库分表 |
| 存储超单库上限 | 分片,提前规划分片键 |
| 要求 99.99% 可用 | 多可用区、主备、自动故障转移 |
| 有明显热点 | 本地缓存 + 多副本 + 防击穿 |
论估算-决策映射是设计的"翻译层"
很多人会算 QPS,但不会"用"。记住这条链路:数字 → 瓶颈在哪 → 哪块积木解这个瓶颈。读多就缓存、写多就异步、存不下就分片、要稳就冗余。决策不再靠感觉,而是对估算的回应。
一致性与可用性:分布式里的"鱼与熊掌"
单机数据库帮你保证了很多事,一旦数据跨多机,"大家都看到一样的数据吗"就成了要主动设计的选择。这一章讲清 CAP、ACID/BASE、一致性模型,以及多副本怎么达成一致(Raft)。
CAP 定理:分区不可避免,只能在 C/A 间取舍
一致性 C(所有节点同一时刻看到相同数据)、可用性 A(每个请求都有响应)、分区容忍 P(网络断了还能跑)。网络分区在分布式里不可避免,所以实际是在 CP(保一致)和 AP(保可用)间取舍。
| 类型 | 分区时行为 | 典型系统 |
|---|---|---|
| CP | 拒绝不一致写入(可能不响应) | ZooKeeper、etcd、HBase |
| AP | 仍响应,但可能返回旧数据 | Cassandra、DynamoDB、DNS |
| CA(理论) | 不允许分区,只能单机 | 单库事务(非分布式) |
现实里不是非黑即白:很多系统在正常情况下同时保 C 和 A,只有发生分区时才退化成 CP 或 AP。而且"一致性"本身有强弱之分(见下一节)。别把 CAP 当教条,把它当"分区时你优先保哪边"的思考框架。
ACID vs BASE:两种哲学
单体关系库讲 ACID(原子性、一致性、隔离性、持久性),强一致、好写。分布式为了可用和扩展,常转向 BASE:基本可用、软状态、最终一致。
论什么时候必须 ACID,什么时候能 BASE
钱、库存、订单状态——这些不能错的用 ACID(或分布式事务兜底)。点赞数、阅读量、推荐流——晚几秒一致无所谓的,用 BASE 换扩展性和可用性。设计的核心就是"给每个数据挑合适的保证级别"。
一致性模型:强一致 / 单调读 / 最终一致
"一致性"不是开关,而是一档档的谱。越强的保证体验越好、代价越高。
| 模型 | 含义 | 例子 |
|---|---|---|
| 强一致 | 写后读一定看到新值 | 主库直读、ZooKeeper |
| 单调读 | 不会"读回旧值"(不会倒退) | 用同一副本/sticky |
| 最终一致 | 无新写后,过段时间全一致 | DNS、Cassandra、MQ 异步 |
| 因果一致 | 有因果关系才保序 | 评论+回复 |
共识算法 Raft:多副本怎么"说了算"
多副本要就"谁是 leader、日志怎么对齐"达成一致。Raft 用"选举 + 日志复制"把这件难事讲成人话:节点分 Leader/Follower,写请求走 Leader,日志复制到多数派才算提交。etcd、Consul、很多 K8s 组件底层都是它。
论为什么是"多数派"而不是"全部"
要求全部节点同意,一个慢节点就能卡死系统(可用性差)。要求多数派(N/2+1)同意,既能防止"两个冲突写入都生效"(脑裂),又容忍少数节点故障。5 节点挂 2 个仍可用,挂 3 个才停——这就是用冗余换可用性的数学表达。
分布式事务:2PC / Saga / TCC
跨服务/跨库要"要么都成、要么都回"时,没有单机事务兜底,得自己编排。三种主流方案各有代价。
| 方案 | 思路 | 适用 / 代价 |
|---|---|---|
| 2PC | 准备→提交,两阶段 | 强一致,但协调者易成瓶颈/阻塞 |
| Saga | 一串本地事务 + 补偿 | 长流程、高可用优先(最终一致) |
| TCC | Try-Confirm-Cancel | 业务可拆分预留/确认,侵入大 |
2PC 在协调者故障时会长时间锁资源;Saga 补偿写起来容易漏边界;TCC 要业务方配合拆三步。优先用本地事务 + 可靠消息(事务消息/发件箱模式)达到最终一致,把"强一致"范围缩到最小。强一致是奢侈品,按需使用。
事务隔离级别:脏读 / 不可重复读 / 幻读
ACID 里的 I(隔离性)指"并发事务互不干扰",但完全隔离太慢,所以数据库给出几档可选级别——隔离越严,正确性越好、并发越差。面试常考这三档现象:
| 级别 | 防脏读 | 防不可重复读 | 防幻读 |
|---|---|---|---|
| 读未提交 | 否 | 否 | 否 |
| 读已提交 RC | 是 | 否 | 否 |
| 可重复读 RR | 是 | 是 | 否(MySQL 用 MVCC 实际防住) |
| 串行化 | 是 | 是 | 是(但最慢) |
论为什么默认不是"串行化"
串行化等于把并发变串行,吞吐量暴跌。多数业务用读已提交或可重复读就够,靠 MVCC(多版本并发控制)让读写不互相阻塞:写时留旧版本,读按快照取,既隔离又不锁读。理解隔离级别,你才懂"为什么我读到了别的事务中间状态"这类诡异 bug 从哪来。
缓存 / CDN / 负载均衡:性能的"三把斧"
性能优化的第一杠杆是别让请求打到慢的地方。离用户越近、离磁盘越远的缓存越值钱:CDN 缓存静态资源在边缘,Redis 缓存 DB 查询结果在内存,负载均衡把流量分摊到多机。三者常配合出现。
缓存的位置与层次
缓存是个"金字塔":越往上越快越小越贵。典型层次:浏览器缓存 → CDN → 反向代理缓存 → 应用本地缓存 → 分布式缓存(Redis) → 数据库缓冲池。
论缓存为什么"快"
内存比磁盘快几个数量级,且缓存层通常无复杂索引/事务。把热点数据请进内存,等于把"每次都问慢的 DB"变成"大多数时候问快的内存"。但缓存引入了一致性问题——数据改了,缓存里的旧值怎么办?看下一节。
缓存三大坑:穿透 / 击穿 / 雪崩
这三兄弟是面试和事故常客,必须分清:
| 坑 | 现象 | 解法 |
|---|---|---|
| 穿透 | 查不存在的 key,直击 DB | 空值缓存 / 布隆过滤器 |
| 击穿 | 某热点 key 过期瞬间海量请求涌向 DB | 互斥锁重建 / 逻辑过期 |
| 雪崩 | 大量 key 同时失效 | 过期时间加随机抖动 |
缓存更新策略:Cache Aside 最常用
缓存和 DB 怎么保持一致?业界最常用 Cache Aside(旁路缓存):读时回填,写时先更新 DB、再删缓存(不是更新缓存,避免并发写错序)。
顺序是坑点:若先删缓存、再更 DB,期间另一读线程可能把旧值重新填回缓存,造成长期不一致。更稳妥是"先更 DB、再删缓存",并配合短暂的"延迟双删"兜极端并发。没有完美顺序,只有更优。
CDN:把静态内容推到边缘
CDN 把图片/JS/视频缓存到离用户最近的边缘节点,用户就近取货,源站压力与跨地域延迟骤降。它与缓存同理,只是位置在"网络边缘"。
负载均衡:L4 vs L7
流量先到负载均衡器,再分摊到多台后端。L4 只看 IP/端口(快),L7 能看 URL/Header/Cookie(聪明,可做按路径路由、限流、TLS 卸载)。
| 维度 | L4 | L7 |
|---|---|---|
| 决策依据 | IP、端口 | URL、Host、Cookie |
| 性能 | 极高 | 略低(解析应用层) |
| 典型 | LVS、云 NLB | Nginx、Envoy、云 ALB |
负载均衡器本身不能成单点——通常做主备(Keepalived/VRRP)或用云厂商托管。否则它一挂,后面再健康也白搭。高可用是从 LB 这一层就开始设计的。
一致性哈希:扩容友好的分片
缓存集群扩缩容时,普通取模 hash(key)%N 会让几乎所有 key 重新映射,引发瞬时雪崩。一致性哈希把节点和数据放到哈希环上,新增节点只影响邻近少量 key。
缓存命中率与容量规划
缓存不是"加了就快",要看命中率(Hit Rate)。命中率 = 命中次数 / 总次数,低于 80% 通常说明缓存策略或容量有问题。容量也要估:热点数据全集能否放进内存?
① key 带随机参数(如每次加 timestamp),每个 key 都不同,永远不命中;② 缓存时间太短,还没热就过期;③ 热点突变,新爆款没预热。命中率要当监控指标盯着,掉下去第一时间查这三样。
数据库扩展:从单库到分片
单库是起点,但不是终点。流量涨起来,先读写分离(主写从读),再不行就分库分表(Sharding)。每一步都引入新复杂度——分片键的选择尤其要命。
垂直拆分与读写分离
第一步往往是"按业务拆库"(用户库、订单库)和"主从复制":一主多从,写走主库、读走从库,把读压力摊出去。这是成本最低、收益最高的扩展。
主从复制:原理与延迟
主库把写操作记成 binlog,异步发给从库重放。问题来了:复制有延迟,刚写的数据立刻从从库读可能读不到("读己之写"失效)。
| 复制模式 | 特点 | 一致性 |
|---|---|---|
| 异步复制 | 主提交即返回,从后追 | 可能丢/读延迟 |
| 半同步 | 至少一个从确认收到 | 折中 |
| 全同步 | 所有从确认 | 强,但慢 |
用户改了昵称,页面刷新却还是旧的——因为读请求被路由到还没追上复制延迟的从库。解法:写后短暂读主库(写主读主一小段时间)、或关键读强制走主、或业务上接受短暂不一致。别假设"写进去立刻读得到"。
分库分表:分片键怎么选
单库到容量/连接上限,就按分片键把数据拆到多库多表。分片键决定数据怎么分布,选错后期极痛。
论分片键要贴合"最主要的热点查询路径"
选了"按地区"但查询多按"用户",会变成全库广播(每片都查一遍)。选键要让绝大多数查询能靠键定位到单一分片。且尽量不可逆——后期想换分片键,意味着数据大迁移,是工程噩梦。
分片后的难点:join / 事务 / 全局 ID
分片省了单库压力,但把"简单的事"变难:跨片 join 要应用层做、跨片事务只能上分布式事务、自增主键会撞。
分片是"最后手段",不是"时髦标配"。读写分离 + 缓存 + 加索引往往能扛到很大体量。过早分片把 join/事务/运维复杂度提前引入,是过度工程的高发区。先量再分。
选型:NoSQL 何时才有必要
不是"分片就上 NoSQL"。关系库仍是默认;只有在特定痛点时才引入专用存储。
| 需求 | 合适选型 |
|---|---|
| 强一致事务、复杂查询 | 关系库(PostgreSQL/MySQL) |
| 海量 KV、超高分片扩展 | KV(Redis、DynamoDB) |
| 写多、按时间查、列可变 | 列族/宽表(Cassandra、HBase) |
| 文档结构、灵活 schema | 文档库(MongoDB) |
| 关系图谱 | 图库(Neo4j) |
连接池、慢查询与索引:DB 的日常命门
分片解决"容量",但日常让 DB 先挂的往往是连接耗尽和慢查询。应用直连 DB 每次建连开销大,必须走连接池(如 HikariCP);慢 SQL 不建索引会把单库 CPU 打满,拖垮所有请求。
很多人以为"池越大并发越高",结果连接数爆炸,DB 侧上下文切换和内存压力反成瓶颈,吞吐下降。经验值:池大小 ≈ (核心数 × 2) + 有效磁盘数,再压测调。比"拍大"有用的是加索引 + 缓存,从源头减少 DB 压力。
消息队列与异步:解耦、削峰与可靠性
把"当下必须同步做完"变成"丢进队列稍后做",系统立刻解耦且能扛突发流量(削峰填谷)。代价是引入最终一致性,要处理消费失败、重复消费。这是高吞吐系统的标配积木。
为什么异步:解耦与削峰
同步调用像"打电话"——对方不接你就卡住。异步像"发短信"——发出去就继续干别的,对方有空再处理。两个好处:
- 解耦:下单不直接调库存/积分/通知,只发消息,下游各自演进。
- 削峰:秒杀瞬时 10 万请求,队列按消费能力匀速处理,后端不被冲垮。
消息模型:topic / queue / consumer group
不同 MQ 模型略异,但核心概念相通:生产者发到 Topic/Queue,消费者组里多个实例分摊消费,一条消息在同组内只被一个实例处理(实现并行)。
论分区数决定并行上限
Kafka 里一条消息只进一个分区,一个分区只被组内一个消费者独占。所以消费者实例数超过分区数时,多余的实例闲着。设计 Topic 分区数时要预估峰值并行度,并留扩容余量(分区只能增不能随便减)。
可靠性:at-least-once / exactly-once
网络不可靠,消息可能丢、可能重。MQ 通常保证至少一次(at-least-once):可能重复投递,但尽量不丢。强求精确一次(exactly-once)要靠"幂等 + 事务消息/去重表"。
| 语义 | 含义 | 代价 |
|---|---|---|
| at-most-once | 最多一次,可能丢 | 最简,但丢数据 |
| at-least-once | 至少一次,可能重复 | 常见默认,靠幂等兜底 |
| exactly-once | 恰好一次 | 复杂(事务/去重),开销大 |
幂等:重复消费的安全带
既然 at-least-once 会重复,消费者必须幂等:同一消息处理多次,效果和一次一样。否则"扣款"重复一次就多扣一次。
关键是"判重"和"业务写入"必须在同一事务/同一原子操作里,否则并发下仍可能都通过判重。用唯一约束(msg_id 唯一索引)让数据库帮你保证原子性,比应用层 if 可靠。
顺序消息与延迟消息
有些业务要顺序:同一订单的状态变更不能乱序。Kafka 靠"同一 key 进同一分区"保分区内有序。延迟消息(如 30 分钟后关单)可用 MQ 的延迟队列或外部定时器。
选型:Kafka vs RabbitMQ vs Pulsar
没有最好的 MQ,只有最合适的。
| MQ | 强项 | 适用 |
|---|---|---|
| Kafka | 高吞吐、持久化、流处理 | 日志、事件流、大数据管道 |
| RabbitMQ | 灵活路由、低延迟 | 任务队列、RPC、复杂路由 |
| Pulsar | 存算分离、多租户、分层存储 | 云原生、超大规模 |
论别为"可能用上"的feature选复杂方案
中小团队、任务队列场景,RabbitMQ 够用且好运维;只有真到"每秒几十万事件 + 流处理"才上 Kafka。Pulsar 能力强但运维重。选你团队运维得起的,而不是 benchmark 上最快的。
死信队列与重试策略
消费失败不可避免:下游临时抖、消息格式错。直接丢弃会丢数据,无限重试会堵队列。死信队列(DLQ)是标准解法:重试 N 次仍失败,消息转进 DLQ,主队列继续往前走,人工/定时去 DLQ 排查。
论重试必须配合退避和幂等
对"正在故障"的下游疯狂重试,等于持续补刀(重试风暴)。指数退避 + 抖动让重试请求错开;同时消费者必须幂等(见上节),因为退避重试必然产生重复投递。DLQ 保证"坏消息不阻塞好消息",是异步系统的安全网。
设计案例:把积木拼成系统
前面是零件,这一章组装。我用三个高频案例演示"方法论怎么落地",并在最后把高可用三板斧(限流/熔断/降级)和权衡清单收口——这是把设计从"能跑"变成"扛得住"的关键。
方法论回顾 + 画图习惯
拿到任何需求,固定走这套:①澄清需求→②估算→③定义接口→④由外到内逐层加积木(LB→Web→缓存→DB→MQ)→⑤写清每处 trade-off。画图从"一个框"开始,别一上来画 30 个组件。
案例一:短链系统
需求:长 URL 转短码,访问短码 302 跳原 URL,高读低写。按方法论走:
论短链的两个设计点
① 短码生成:自增发号器+base62 最省空间且不可猜;哈希法可能冲突需重试。② 302 还是 200:302 跳转,原 URL 变更/统计都灵活;若想让 CDN 缓存"展开结果"可用 200+前端跳,但失去服务端统计。按"要不要统计点击"选。
短链常被用来隐藏恶意网址/钓鱼。要加:生成频率限制、可疑 URL 黑名单、点击时扫毒。否则你的短链服务会变成黑产跳板,域名被墙。
案例二:Feed 流(信息流/朋友圈)
需求:用户刷"关注的人发的动态",读多写少、要新鲜。两种经典方案:
| 方案 | 做法 | 优劣 |
|---|---|---|
| 拉(Pull) | 刷时查所有关注者的新动态再聚合 | 实时,但大 V 粉丝多时查询爆炸 |
| 推(Push) | 发动态时写进每个粉丝的收件箱 | 读快,但大 V 发一条写爆(扇出) |
| 推拉结合 | 大 V 用拉、普通用户用推 | 折中,工程复杂但扛得住 |
案例三:秒杀与限流
需求:瞬时海量请求抢有限库存,不能超卖、不能压垮。核心思路:层层削峰 + 原子扣减。
几万人同时 SELECT stock FROM ... FOR UPDATE,数据库瞬间被行锁打死。正确姿势:请求先进 Redis 原子扣减(内存级、快),只有扣到名额的才进 DB 建单。把"是否还有货"的判断挡在离用户最近、最快的地方。
高可用三板斧:限流 / 熔断 / 降级
系统总会遇到超出预期的压力或故障。高可用不是"不挂",而是"挂得优雅"——把影响圈在最小范围。
| 手段 | 目的 | 常见实现 |
|---|---|---|
| 限流 | 保护容量不被突破 | 令牌桶/漏桶、Nginx limit_req |
| 熔断 | 防故障扩散(雪崩) | Sentinel、Hystrix 思路 |
| 降级 | 保主链路 | 开关/兜底数据/默认值 |
| 幂等 | 重试/重复安全 | 唯一约束/去重表(见第 5 章) |
论为何限流是"对用户的温柔"
不限流,系统被冲垮,所有用户都用不了;限流,只拒绝超额的那部分,大多数用户正常。拒绝 1% 好过崩给 100%。所以限流不是拦用户,是保住能服务的那批用户——前提是配好阈值和友好提示。
权衡清单:每个选择都写代价
好设计的标志不是"用了多炫的技术",而是清楚每处取舍。交卷前过一遍:
上线前压测:用数据验证设计
设计再漂亮,没压测就是猜想。上线前用工具模拟目标峰值,看架构在哪一层先跪——是连接池满、缓存击穿、还是 DB 锁。
论压测要"带真实数据分布"
用均匀随机数据压,往往压不出问题——真实流量有热点(大 V、爆款)。压测要造出贴近真实的热点分布,才能暴露缓存击穿、单分片过热。压测结论 + 监控证据,才是对"设计对不对"的最终回答。
系统设计先澄清功能/非功能需求与量级(QPS、存储、读写比、SLA 几个 9)再动手。分布式下在 CAP 中取舍(P 不可避免,选 CP 或 AP),用 ACID/BASE 选保证级别,靠 Raft 做多副本共识,用 Saga/2PC 管跨服务事务。性能三板斧:CDN 边缘缓存、Redis 缓存(警惕穿透/击穿/雪崩,用 Cache Aside)、L4/L7 负载均衡(一致性哈希扩容友好)。数据库先读写分离、主从复制(注意复制延迟),再按分片键分库分表(避开过早分片)。消息队列解耦削峰,用 at-least-once + 幂等兜底,Kafka/RabbitMQ/Pulsar 按场景选。高可用靠限流/熔断/降级/幂等"挂得优雅"。案例(短链/Feed/秒杀)与权衡清单把积木拼成可扛的系统。
1.缓存"穿透、击穿、雪崩"三者区别是什么?各怎么防?
查看答案
穿透=查不存在的 key 打穿到 DB(布隆过滤器/空值缓存);击穿=单热点 key 过期瞬间海量请求涌向 DB(互斥锁重建/逻辑过期);雪崩=大量 key 同时失效(过期时间加随机抖动)。三者都围绕"别让请求无缓冲地砸到慢的 DB"。
2.CAP 里的 P(分区容忍)为什么"必须保"?CP 和 AP 怎么选?
查看答案
网络分区(节点间失联)在分布式里不可避免,所以 P 是前提,只能在 C(强一致)和 A(高可用)间取舍:CP 系统分区时拒绝不一致写入(如 etcd),AP 系统分区时仍响应但可能返回旧数据(如 Cassandra)。选哪边看业务——钱/库存要 CP,点赞/feed 可 AP。
3.为什么分布式系统尤其强调"幂等"?怎么实现?
查看答案
网络不可靠,请求会被重试、消息会被重复投递(at-least-once)。若"扣款"不幂等,重复一次就多扣一次。实现:用 msg_id 唯一索引/去重表,让"判重"和"业务写入"在同一事务里原子完成,保证多次处理等同一次。
4.分片键(sharding key)选错会有什么后果?怎么选?
查看答案
选错会让主要查询变成全库广播(每片都查),失去分片意义,且后期换键=数据大迁移。应选贴合最主要热点查询路径的字段(如 user_id),让多数查询能靠键定位到单一分片;尽量选不可逆的键。
5.秒杀系统为什么不能"直接查数据库库存"?正确思路是什么?
查看答案
几万人同时 SELECT ... FOR UPDATE 会瞬间把数据库行锁打死。正确:请求先在 Redis 用原子递减(DECR)判是否抢到名额,只有抢到的才进 MQ 异步建单;库存挡在最快的内存层,层层削峰,DB 只处理真正成交的少量写。
6.主从复制下,"刚写完马上读"可能读到旧值,为什么?怎么缓解?
查看答案
因为复制是异步的,从库追 binlog 有延迟。缓解:写后短暂强制读主库、关键读走主、或业务接受短暂不一致。别假设"写进去立刻读得到"。
下一步往哪走
路学完系统设计之后
① 串技术栈:去 tech-middleware 看 Nginx/Kafka/Redis 真家伙,本页是"为什么用"、那里是"怎么配"。
② 配合网络:回看 tech-network 的 LB/CDN/TLS,本页的架构决策要靠那些协议落地。
③ 动手:挑一个需求(短链/秒杀/评论系统)完整走一遍方法论,画架构图并写下每处的 trade-off——能写清取舍,才算真懂。
算法复杂度分析:大 O 与大白话
复杂度衡量的是「输入规模 n 增长时,资源(时间/空间)的增长趋势」,不是精确运行次数;用大 O 记法忽略常数和低阶项,方便在写代码前就能比较两种思路谁更扛得住大数据。
为什么需要复杂度
同一个功能,双循环和哈希表两种写法在小数据上几乎看不出差别,但 n 到十万、百万时差距就是"秒回"和"卡死"。复杂度给你一个不依赖机器、不依赖语言的可比标尺。
论常见阶对照
记住几档典型量级:O(1) 哈希表读写(均摊);O(log n) 二分查找、平衡树;O(n) 线性扫描;O(n log n) 归并排序、快排平均;O(n²) 冒泡、朴素双重循环;O(2ⁿ) 朴素斐波那契递归、子集枚举。最好/最坏/平均情况要分开看,例如快排平均 O(n log n)、最坏 O(n²)(已排序且首元素为轴)。空间复杂度同样重要,递归深度、额外数组都算。
| 复杂度 | 典型场景 |
|---|---|
| O(1) | 哈希表读写(均摊) |
| O(log n) | 二分查找、平衡树 |
| O(n) | 线性扫描 |
| O(n log n) | 归并排序、快排(平均) |
| O(n²) | 冒泡排序、朴素双重循环 |
| O(2ⁿ) | 朴素斐波那契递归、子集枚举 |
论主定理简述
对分治递归 T(n)=aT(n/b)+f(n),主定理给三种归并情形,可秒判很多递归的阶,不必手推(如归并/快排/二分)。
复杂度是渐进上界,常数因子在小 n 时可能主导;别盲目追求理论最优而忽略实际常数与缓存局部性。n 很大时低阶项无所谓,但 O(n²) 在 n=10⁵ 会直接爆,优先选 O(n log n)。
Q1. 二分查找为什么是 O(log n)?
参考答案
每次把搜索区间对半砍,k 次后剩 1 个元素,即 2^k≈n,所以 k≈log₂n。
动手片段
下面这段可直接运行,把抽象概念变成"手感"。