← 学习门户 每日面试题 学习看板

2026-10-08

架构决策与权衡 —— 从设计模式到架构哲学

10 题 · 平均 3.3/5 · low 3.33 · medium 3.25 · high 3.33 · 第 1 轮

#1 什么是设计模式?有什么好处? low system-design

题目

什么是设计模式?有什么好处?

我的回答

所谓设计模式,设计模式就是把一些经常使用的经常使用的逻辑整合成一种模式,然后重复的使用,方便传递,方便重复使用。它其实是一套思维逻辑,并不仅仅是一个工具,它们传递的是一种种思想。常见的设计模式主要分几类,创建型的、逻辑型的、管理对象型的。比如创建型的有创建建造者、工厂、模板方法、工厂方法等。逻辑型的像责任链、策略、代理,这都是相当于行为或者逻辑型的。好像单例就是创建对象型的。

点评

优点:抓住了设计模式的本质——"思维逻辑"、"传递思想",这个洞察到位;列举的具体模式大多正确。
不足:分类体系错位(GoF 三大类是创建型/结构型/行为型,不是"逻辑型/管理对象型");模板方法归错类(应为行为型);代理归错类(应为结构型);遗漏原型、抽象工厂、适配器、装饰器等常见模式。
补充:完整分类——创建型 5(单例/工厂方法/抽象工厂/建造者/原型)、结构型 7(适配器/桥接/组合/装饰器/外观/享元/代理)、行为型 11(责任链/命令/迭代器/中介者/备忘录/观察者/状态/策略/模板方法/访问者/职责链)。设计模式四大价值:代码复用、开闭原则、高内聚低耦合、扩展性。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对设计模式本质认知(不是记住名字)+ GoF 标准分类体系掌握。

**知识点拆解**:
1. **本质**:设计模式是对反复出现的软件设计问题的**通用解决方案模板**,是**思维模型**而非代码片段,其内核是"用变化的对象封装变化的原因"(开闭原则的具象化)
2. **分类体系**:GoF 1994 年提出 23 种模式,按**创建对象/组织对象/使用对象**三维度分为**创建型(5)/结构型(7)/行为型(11)**
3. **四大核心价值**:
- **代码复用**:模板方法复用算法骨架、享元复用共享对象
- **开闭原则**:策略/工厂方法/抽象工厂让扩展不需修改
- **高内聚低耦合**:外观隔离复杂子系统、中介者去耦交互对象
- **可扩展**:观察者/责任链支持动态增加处理节点

> 按「本质认知 → 分类体系 → 工程价值」递进。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清"设计模式 vs 代码框架 vs 设计原则"三者的层次关系
- **广度**:GoF 之外了解哪些模式(如 TOC 并发模式、企业集成模式、架构模式 MVC/MVVM)
- **应用**:能否在项目里举出使用设计模式的真实场景

**延伸递进面试题**:
1. **设计模式和使用原则(SOLID)是什么关系?**
- 解答要点:SOLID 是原则(Why),设计模式是原则的常见实现(How),比如策略模式实现 ISP,模板方法体现 LSP
2. **模板方法模式和策略模式的区别是什么?如何选型?**
- 解答要点:模板方法是编译期决定(继承+方法重写),策略模式是运行期决定(组合+对象注入);算法骨架固定用模板方法,算法可运行时切换用策略
3. **你项目里用过哪些设计模式?解决了什么问题?**
- 解答要点:能举出 2-3 个具体场景(如工作流用责任链、支付通道用策略、事件通知用观察者),并说清为什么不用其他方式

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,设计模式本质上是一套**被反复验证过的软件设计思维模板**,它解决的是反复出现的设计问题,让我在遇到类似场景时不用从零开始思考。它传递的不只是代码结构,更是一种"如何应对变化"的设计哲学——用变化的对象封装变化的原因。

我理解的经典分类是 GoF 提出的 23 种,分三大类:**创建型**(单例、工厂方法、抽象工厂、建造者、原型)解决对象怎么造;**结构型**(适配器、桥接、组合、装饰器、外观、享元、代理)解决对象怎么组装;**行为型**(责任链、命令、观察者、状态、策略、模板方法等)解决对象怎么协作。

设计模式的价值我总结为四点:一是**代码复用**,比如模板方法把公共流程固定下来;二是**开闭原则**,策略和工厂让新增类型不改老代码;三是**高内聚低耦合**,外观模式和中介者模式把复杂子系统包起来;四是**扩展性**,观察者和责任链支持运行时动态增删处理节点。

在我的项目里,支付通道用策略模式扩展不同支付方式、审批流用责任链模式串起多级审核、事件通知用观察者解耦业务和消息推送,都是设计模式落地的典型场景。设计模式不是炫技,而是在合适场景下用**最少的复杂度换最多的灵活性**。

#2 请简述MVC模式的思想 low system-design

题目

请简述MVC模式的思想

我的回答

MVC 是最我们最常用的一个模式,它主要是用来进行分层设计,将整个项目划分成几个层次,对外展现,然后业务设计跟自定义处理。对外展现方面,通过 Web HTML 给外部展现出相应的内容。MVC 很久以前最早接触的其实是分层,通过 Spring MVC 去进行的。它通过 Controller 然后去进行控制,然后通过设计 Model 去进行模型的设计。通过 Web 去展现数据。这就是一个标准的 MVC,将整个项目划分成几个层次,展现、控制、设计,分别进行,分别管控自己相应的事情,将项目进行很好的分隔,也更好的管控跟开发,在不同的阶段去做不同的事。

点评

优点:抓住分层设计和关注点分离的核心;能对应到 Spring MVC 实践;三个组件都能列出。
不足:三个组件职责描述模糊(Model 被说成"模型设计"、View 局限于 HTML);没讲清数据流向(Controller 是桥梁);没提核心思想"关注点分离";未涉及 MVC/MVP/MVVM 变体。
补充:标准数据流 `用户→Controller→Model→Controller→View→用户`;变体对比 MVP(Presenter 替代 Controller)/MVVM(ViewModel 双向绑定);Spring MVC 落地:DispatcherServlet→Handler→Service/DAO→JSP/Thymeleaf。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 MVC 三组件职责和数据流的清晰认知,以及 MVC 与 MVP/MVVM 的辨别能力。

**知识点拆解**:
1. **三组件职责**:
- **Model**:封装**业务数据 + 业务逻辑**,独立于 UI,是系统的"大脑"(Service/Entity/DAO 层)
- **View**:负责**数据渲染**,独立于业务,是系统的"皮肤"(HTML/JSP/Thymeleaf/Vue/React 组件)
- **Controller**:**中介者**,接收请求→调用 Model→选择 View→返回响应
2. **核心数据流**:`用户请求 → Controller(解析、校验)→ Model(业务处理、持久化)→ Controller(组装响应数据)→ View(渲染)→ 用户`
3. **核心思想**:**关注点分离(SoC)**——UI 变化不影响业务、业务变化不影响 UI,各层独立演进

> 按「职责定义 → 数据流向 → 设计思想」递进。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清 Controller 在 MVC 中的"调度者"角色,而非"业务承载者"
- **广度**:MVC vs MVP vs MVVM 三种架构模式的取舍
- **应用**:Spring MVC 具体到 DispatcherServlet/Handler/Adapter 等组件的落地

**延伸递进面试题**:
1. **MVC、MVP、MVVM 有什么区别?各自适用场景是什么?**
- 解答要点:MVC 中 View 可观察 Model 变化;MVP 中 Presenter 承担逻辑、View 变胖;MVVM 中 ViewModel 与 View 双向绑定,适合前端(Vue/React);后端 REST API 一般不用 MVC 模式,而是 Controller-Service-DAO 分层
2. **Spring MVC 的完整请求处理流程是怎样的?**
- 解答要点:浏览器请求→DispatcherServlet→HandlerMapping→Handler(Controller)→HandlerAdapter→业务处理→ViewResolver→View 渲染→响应
3. **为什么现代前端框架普遍用 MVVM 而不是 MVC?**
- 解答要点:MVVM 双向绑定减少手动同步,前端 View 需要频繁响应数据变化;MVVM 更适合声明式 UI;Spring 后端不需要 View 层参与业务

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,MVC 是一种经典的**分层架构模式**,核心思想是**关注点分离**——把 UI、业务逻辑、数据持久化分成三层,各层独立演进。

三个组件职责很清晰:
- **Model** 是"大脑",封装业务数据(Entity)和业务逻辑(Service),完全独立于 UI;
- **View** 是"皮肤",只负责数据渲染,不理解业务,可以是 JSP、Thymeleaf、Vue、React 组件;
- **Controller** 是"中介者",接收用户请求,调用 Model 处理业务,再把结果交给 View 渲染返回。

数据流向是:`用户 → Controller(解析校验)→ Model(业务处理)→ Controller(组装数据)→ View(渲染)→ 用户`。

在 Spring MVC 里的落地是:浏览器请求 → `DispatcherServlet`(前端控制器)→ `HandlerMapping` 找到对应的 `@Controller` 方法 → 方法调用 Service/DAO(Model 层)→ 通过 `ViewResolver` 选择 JSP/Thymeleaf 渲染 → 返回响应。

MVC 有两个常见变体:
- **MVP**(Model-View-Presenter):Presenter 替代 Controller,View 变"胖",逻辑集中在 Presenter;
- **MVVM**(Model-View-ViewModel):ViewModel 与 View **双向数据绑定**,适合前端框架(Vue/React),减少手动同步代码。

现代后端 API 一般不再严格用 MVC,而是用 Controller-Service-DAO 分层;前端 Vue/React 则普遍用 MVVM。MVC 的价值不在于套用模式,而在于让每一层职责清晰、可独立测试、可独立演进。

#3 为什么不建议用数据库唯一性约束做幂等控制? low system-design

题目

为什么不建议用数据库唯一性约束做幂等控制?

我的回答

所谓幂等,就是为了保证同样的请求过来多次,得到同样的结果,而不是多次请求之后得到不同的结果。这不仅仅是数据库层面要做的,而是在业务层也要做相应的处理。第一,你要确定相同,这个请求是相同的。你需要通过特定的业务模式或者什么的去保证了解这个请求跟上一个请求或者上几个请求是一样的,来判定这个请求已经来过了,而不再进行新的处理,或者处理之后得到一样的结果。用数据库唯一约束做索引,这种情况只适用于幂等的少数一些情况,而不会涉及所有,不能统一的处理幂等的逻辑。并不能真正保证请求的幂等。想要达到幂等,需要包括识别、特殊处理,然后保证。其实核心应该在识别跟处理两个项目上,然后通过这两项最终保证每次请求的结果是同样的。比如查询,你每次查询使用相同结果,在没有数据更新的情况下都会是一样的。但是更新操作就不一定是怎么回事了,比如扣减,每次扣减库存,如果连续的进行扣减,就会导致扣减多次,最终库存扣错。这时候的幂等你就没法通过数据库的唯一性约束去保证这个幂等。你需要通过请求的业务识别,识别到这个请求是跟之前重复的,才能保证你的幂等性,也就是说这次不会出现重复的扣减。这就是我理解的为什么不要用数据唯一性做幂等。

点评

优点:抓住幂等本质(相同请求相同结果);区分业务层识别 vs 数据库约束;举例到位(库存扣减);三段式框架(识别→处理→保证)。
不足:未说清唯一约束的技术局限(只适用于写路径、异常处理侵入、锁竞争、无业务语义);未提主流幂等方案(去重表/Redis SETNX/Idempotency-Key/乐观锁);未区分"不能用" vs "不够用"。
补充:唯一约束可用于订单号防重等插入型场景,但读操作/更新操作/分布式场景都无效;主流方案:业务去重表、Redis SETNX+TTL、Idempotency-Key 请求头、乐观锁版本号、状态机单向流转。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对幂等本质的理解 + 唯一约束的能力边界 + 多种幂等方案的选型能力。

**知识点拆解**:
1. **幂等本质**:**同一操作执行 N 次,结果与执行 1 次等价**。分三类:
- **天然幂等**:查询、`PUT /user/1`(覆盖式)
- **业务幂等**:下单、扣款(需业务保证)
- **状态幂等**:状态机单向流转(`未支付→已支付` 不会重演)
2. **唯一约束的能力边界**:
- ✅ 能做:插入型去重(订单表 `uk_order_no`、支付流水 `uk_trade_no`)
- ❌ 不能做:读操作(无写入动作)、更新操作(`UPDATE stock -1` 每次都会执行)、分布式(多实例并发写入)、无业务语义(不记录请求上下文)
- ⚠️ 副作用:唯一冲突抛异常、锁竞争热点、代码侵入(try-catch)、无法记录"这次请求属于哪个业务上下文"
3. **主流幂等方案对比**:
- **业务去重表**:`unique_key + request_id + status + result`,简单可靠,适合强一致
- **Redis SETNX + TTL**:`SET key value NX EX 5`,高性能,适合短时防重复提交
- **Idempotency-Key 请求头**:API 层统一拦截,Stripe/Twilio/Adyen 都用
- **乐观锁版本号**:`UPDATE WHERE version = ?`,防并发更新
- **业务状态机**:状态只能单向流转,天然幂等(如"订单已关闭"后再来支付请求直接返回失败)

> 按「本质定义 → 唯一约束边界 → 主流方案」递进。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清唯一约束是"数据库能力"、幂等是"业务诉求",两者不是等价物
- **广度**:覆盖读/写/更新/分布式多种场景的幂等方案选型
- **应用**:在支付、下单、消息消费这类高价值场景中,幂等如何分层实现

**延伸递进面试题**:
1. **幂等键(Idempotency-Key)是怎么设计的?如何避免攻击?**
- 解答要点:客户端生成 UUID 或业务单号 + 时间戳;服务端存 key → 结果映射;设置 TTL 防存储膨胀;校验请求体 hash 防止 key 复用不同请求
2. **消息队列消费幂等如何设计?和接口幂等有什么差异?**
- 解答要点:MQ 是"至少一次投递",消费端必须幂等;用 `msgId` 做唯一键存入去重表;跨服务调用要串联幂等
3. **Redis 分布式锁 + 幂等控制,是重复的吗?**
- 解答要点:分布式锁解决"并发互斥"(同一时刻只有一个能进),幂等解决"多次执行结果一致"(进多次也要一致);两者互补:锁在入口防止重复进入,幂等在业务层兜底

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,这个问题我先说结论:**唯一约束能做幂等,但只覆盖很小的场景,不能作为统一方案**。

先讲幂等本质:**同一操作执行 N 次,结果和执行 1 次等价**。它不是数据库能单独保证的,需要业务层识别重复请求、走幂等处理路径。

唯一约束的边界是:
- **只能覆盖插入型操作**——比如订单表加 `uk_order_no` 防止重复下单,这可以用;
- **覆盖不了读操作**——重复查询订单,没写入动作,唯一约束无用武之地;
- **覆盖不了更新操作**——扣减库存是 `UPDATE stock -1`,每次都会执行,不会冲突,会重复扣;
- **覆盖不了分布式场景**——多实例并发,唯一冲突要抛异常、事务回滚、代码侵入强。

而且用唯一约束还有几个副作用:**冲突走异常路径**,代码里到处 try-catch;**唯一索引写入有锁竞争**,热点 key 会拖慢写入;**没有业务语义**,不记录请求的上下文、时间窗、处理结果。

所以主流方案是分层的:
1. **业务去重表**:`idempotent_key + status + result` 存下来,简单可靠,适合强一致场景;
2. **Redis SETNX + TTL**:`SET key value NX EX 5`,防短时重复提交,比如表单提交;
3. **Idempotency-Key 请求头**:API 层统一拦截,Stripe 就是这么做的,客户端传 key,服务端存 key → 结果的映射;
4. **乐观锁版本号**:`UPDATE WHERE version = ?`,防并发更新覆盖;
5. **业务状态机**:状态单向流转(未支付 → 已支付),重复支付请求直接返回失败,天然幂等。

实际项目中通常是**组合使用**——入口用 Redis 挡住短时重放,业务层用去重表 + 状态机保证最终一致性。唯一约束更多是作为**最后一道兜底**(订单号唯一),而不是主力方案。

#4 分布式锁有几种实现方式? medium system-design

题目

分布式锁有几种实现方式?

我的回答

分布式锁常见的几种方式的话,包括 Redis 实现的、ZooKeeper 实现的,还可以用本地消息。还可以用数据库去实现。核心就是确保这个事务整个状态的记录。这个线程过来,其他线程等待,拿到了的就拿,没拿到的就等待。锁的意义无非就是控制外部线程的进入。分布式锁也是这个概念。控制多线程的进入,同时只有一个线程。然后做个标识,一个线程在执行,其他线程不可进,就完事。分布式锁就是找不同的介质去干这个事。要么用 Redis,要么用 Zookeeper,数据库也可以。反正有个外部介质去在多个服务之间去同时的能感知到这个东,这个线程这个锁被持有着,然后管控其他线程不被调用,就可以了。

点评

优点:抓住分布式锁本质(互斥);列举主流实现(Redis/ZK/DB);"找不同介质做同一件事"抽象到位。
不足:"本地消息"不是分布式锁实现方式(应为本地锁或混淆了 MQ 概念);遗漏 etcd/Consul/RedLock;未做选型对比(AP vs CP、性能、死锁友好度);未提具体机制(SETNX/watches/事务)。
补充:主流选型——Redis(AP,高性能,watchdog 防死锁)/ZK(CP,强一致,临时节点自动释放)/etcd(CP,Raft,K8s 生态)/DB(事务,低频场景);核心区别是一致性模型和死锁处理机制。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对分布式锁介质的全面掌握 + 各方案的**一致性/性能/死锁**权衡 + 落地能力。

**知识点拆解**:
1. **四大类介质**:
- **Redis**:`SET key value NX PX 30000`,靠**过期时间 + watchdog 续期**防死锁;主从切换可能丢锁(AP 弱一致);生产用 **Redisson** 封装
- **ZooKeeper**:创建**临时顺序节点**(`/lock/0001`, `/lock/0002`),拿到最小节点者持锁,watch 前一个节点等释放;临时节点**进程崩溃自动删除**天然防死锁;CP 强一致
- **etcd**:基于 Raft,通过**租约 (Lease) + Watch** 实现,K8s 用它做 Leader Election
- **数据库**:`INSERT + 唯一约束`(乐观方式)或 `SELECT ... FOR UPDATE`(悲观行锁);性能差但零依赖
2. **核心权衡三角**:**一致性 / 性能 / 死锁友好度**
- Redis:性能最高、一致性最弱、需要 watchdog 兜底
- ZK:一致性最强、性能中等、天然防死锁
- DB:性能最低、一致性最强、事务保证
3. **争议点——RedLock**:Antirez 提出的"多 Redis 节点同时加锁,多数派成功才算"方案,Marko Elezi 论证过其安全性问题,主流社区已不推荐(Redisson 也弃用了 RedLock)

> 按「介质分类 → 权衡矩阵 → 争议方案」递进。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清 Redis 主从切换丢锁的具体场景、ZK 临时节点的实现细节
- **广度**:对比 Redis/ZK/etcd/DB/RedLock 的差异,理解一致性模型(CP vs AP)对锁的影响
- **应用**:业务场景选型——高并发秒杀用 Redis、Leader 选举用 ZK/etcd、低频用 DB

**延伸递进面试题**:
1. **Redis 分布式锁在主从切换时会发生什么?如何解决?**
- 解答要点:主库加锁后崩溃、从库升主但没同步到锁 key,第二个客户端能拿到锁,出现**双持锁**;RedLock 方案存在争议(客户端时钟偏移);生产实践是接受 AP 弱一致性、业务侧做幂等兜底
2. **ZooKeeper 实现分布式锁的完整流程是什么?为什么不用持久节点?**
- 解答要点:临时顺序节点 + watch 前一个节点;持久节点进程崩溃不会释放导致死锁;顺序节点解决惊群问题
3. **分布式锁加了锁,业务执行超时怎么办?看门狗机制怎么工作?**
- 解答要点:Redisson watchdog 每 (TTL/3) 检查持锁线程是否存活,存活就续期;业务线程崩溃 watchdog 停止续期,锁自然过期;防止"业务卡住但锁持有"的死锁

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,分布式锁常见实现主要有**四大类介质**:

**1. Redis 实现**:`SET key value NX PX 30000` 原子加锁,value 存请求者标识防误删,PX 设过期时间防死锁。生产用 **Redisson** 客户端,带 watchdog 机制——加锁线程每 TTL/3 检查自己是否存活,存活就续期,业务卡住或崩溃时锁自然过期。性能高(毫秒级),但主从切换可能丢锁(AP 弱一致),所以业务侧要做幂等兜底。

**2. ZooKeeper 实现**:每个客户端创建**临时顺序节点**(`/lock/0000000001`, `/lock/0000000002`),序号最小的持锁,其他 watch 前一个节点。进程崩溃临时节点自动删除,**天然防死锁**。CP 强一致,但性能中等,适合 Leader 选举等对一致性要求高的场景。

**3. etcd 实现**:基于 Raft,用**租约 (Lease) + Watch** 做锁。K8s 用它做 Leader Election 就是典型场景,云原生生态友好。

**4. 数据库实现**:`INSERT` 到锁表 + 唯一约束(乐观),或 `SELECT ... FOR UPDATE` 行级锁(悲观)。零外部依赖但性能最差,适合低频场景。

选型建议:**高并发业务锁**(秒杀、库存扣减)用 Redis + Redisson;**Leader 选举、强一致**场景用 ZK 或 etcd;**低频、已有 DB** 直接用 DB。**RedLock**(Antirez 的多节点方案)因为安全性有争议,主流社区已经不推荐用了。

选型的本质是权衡:**一致性 vs 性能 vs 死锁友好度**,没有银弹。

#5 实现一个分布式锁需要考虑哪些问题? medium system-design

题目

实现一个分布式锁需要考虑哪些问题?

我的回答

分布式锁要考虑哪些问题?锁要考虑性能,性能要高一些,一致性。锁的核心也就是一致性嘛,要保证几个线程看到的同一时间的是同样的。一致性对应的还有可用性。但是锁一般保证的是强一致,所以根据 CAP,它会放弃一定的可用性。锁如果出现问题了需要做补偿。另外就是可重入。一个锁在连续锁的情况下,根据单点锁生成的原理要考虑可重入,加几次锁要保证锁是一致的,同一个锁而不是多个锁。然后是加锁要考虑死锁问题,要定期的释放,比如使用 Redis 锁的话,要给锁进行释放,设置过期时间。另外就是如果锁内的持续事务没有完成,要考虑锁的续期。基本上就是这些了。

点评

优点:六维系统思考(性能/一致性/CAP/补偿/可重入/死锁/续期);CAP 权衡理解到位;区分死锁防护与锁续期。
不足:未提锁持有者校验(防误删);未提主从切换丢锁风险;"补偿"表述模糊(锁失败应走重试/幂等兜底而非补偿);遗漏锁粒度、公平性、可观测性;未提选型建议。
补充:安全性 checklist——原子性/持有者标识/过期时间/watchdog 续期/可重入计数;主流方案对比 Redis(AP+watchdog)vs ZK(CP+临时节点)vs etcd(Raft+Lease)。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察分布式锁的**工程化 checklist**——不是"能不能加锁",而是"能不能安全地加锁"。

**知识点拆解**:
1. **正确性维度**:
- **互斥性**:同一时间只有一个持有者
- **持有者校验**:value 存 UUID/线程 ID,解锁前用 Lua 脚本校验 `if value == mine then DEL`,防误删别人的锁
- **加解锁原子性**:`SET key value NX PX 30000` 一条命令原子加锁;解锁用 Lua 脚本"校验+删除"原子执行
- **可重入**:同线程多次加锁累加计数器,`count++` 每次续期,释放时 `count--` 归零才真正删除
2. **可靠性维度**:
- **过期时间**:防持锁进程崩溃导致死锁
- **watchdog 续期**:Redisson 每 TTL/3 检查持锁线程,存活则续期,崩溃则不续期让锁自然过期
- **主从切换风险**(Redis 特有):主库加锁崩溃,从库升主但没同步到锁 key,第二个客户端能拿到锁;ZK/etcd 基于强一致集群无此风险
- **RedLock 争议**:Antirez 提出多节点多数派方案,但 Marko Elezi 论证客户端时钟偏移会导致失效,主流不推荐
3. **可用性维度**:
- **锁粒度**:按业务对象锁(`lock:order:{orderId}`)而非全局锁,减少竞争
- **公平性**:FIFO 排队 vs 抢占式,Redisson 默认非公平
- **性能瓶颈**:单点 Redis 会成为瓶颈,热点 key 需要拆分(如 hash slot)
- **可观测性**:锁等待时间、超时率、加解锁成功率要监控告警
4. **CAP 权衡**:
- 锁一般选 CP(一致性优先),牺牲可用性(Redis 主库挂可能拒绝写入)
- Redis 是 AP 但通过**业务幂等兜底**降级到"最终一致"

> 按「正确性 → 可靠性 → 可用性 → CAP 权衡」四层递进。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清"加解锁原子性"和"持有者校验"的具体实现(Lua 脚本)
- **广度**:对比 Redis/ZK/etcd/DB 在**一致性/性能/死锁友好度**上的差异
- **应用**:业务侧如何为 Redis 锁的丢锁风险兜底(幂等设计)

**延伸递进面试题**:
1. **Redis 分布式锁的解锁 Lua 脚本怎么写?为什么不能分开执行 GET + DEL?**
- 解答要点:`if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end`;分开执行会有竞态——GET 校验通过但 DEL 之前锁刚好过期被其他客户端抢占,DEL 会误删别人的锁
2. **watchdog 机制具体如何工作?如果持锁线程 hang 死了怎么办?**
- 解答要点:Redisson 启动独立看门狗线程,每 TTL/3 检查持锁状态;业务线程 hang 时 watchdog 仍能续期,业务侧需要有超时兜底(如业务线程设置超时时间);业务进程崩溃 watchdog 也停,锁自然过期
3. **分布式锁和幂等控制是什么关系?是重复的吗?**
- 解答要点:分布式锁解决"并发互斥"(同时只能一个进),幂等解决"多次执行结果一致"(进了多次也要一致);两者互补——锁在入口防止重复进入,幂等在业务层兜底;生产实践是"锁 + 幂等"组合使用

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,实现一个分布式锁,我会从**正确性、可靠性、可用性、CAP 权衡**四个维度去考虑。

**正确性**层面:
- **加锁原子性**:`SET key value NX PX 30000` 一条命令原子执行"不存在才设置 + 过期时间";
- **持有者校验**:value 存请求者的 UUID 或线程 ID,解锁必须校验是"自己的锁",防误删别人的锁;解锁用 Lua 脚本 `if GET == mine then DEL` 保证"校验 + 删除"原子;
- **可重入**:同线程多次加锁累加计数器,count 归零才真正删除;
- **锁粒度**:按业务对象加锁,比如 `lock:order:{orderId}` 而不是全局锁。

**可靠性**层面:
- **过期时间**:防持锁进程崩溃导致死锁;
- **watchdog 续期**:Redisson 每 TTL/3 检查持锁线程是否存活,存活续期、崩溃不续;
- **主从切换丢锁**(Redis 特有):主库加锁崩溃、从库升主没同步到锁 key,第二个客户端能加锁——这是 Redis 锁最致命的风险。兜底方案是**业务侧做幂等**,接受 AP 弱一致性;如果对一致性要求极高,用 **ZooKeeper**(临时顺序节点 + 强一致集群)或 **etcd**(Raft + Lease)。

**可用性**层面:
- **锁粒度设计**避免过度串行;
- **可观测性**:锁等待时间、超时率、加解锁成功率监控告警;
- **RedLock 争议**:Antirez 提过多节点多数派方案,但存在时钟偏移问题,主流社区已经不推荐。

**CAP 权衡**:
- 锁本质选 **CP 一致性优先**(宁可短暂不可用也不能多持锁);
- Redis 是 AP 弱一致,通过业务幂等兜底降级到"最终一致";
- 高并发业务(秒杀、库存)用 Redis + Redisson,Leader 选举用 ZK/etcd,低频场景用 DB。

实际项目中通常是**分布式锁 + 业务幂等**组合使用——锁挡并发重复进入,幂等兜底防止极端情况下重复执行。

#6 Leaf 生成分布式 ID 的原理? medium system-design

题目

Leaf 生成分布式 ID 的原理?

我的回答

Leaf 指的是美团的那个生成 ID 的,生成分布式 ID 的算法工具。它生成 ID 应该是有几种算法选择,一个是号段性质的,一个是类似于雪花算法性质的。号段性质的就是根据规则,然后分配号段,然后一次分,分配一批,下一批的话使用完了再申请下一批,确保局部有序。然后选择雪花算法性质的,就是根据机器去生成机器时间戳,并生成自己的分布式 ID。不知道我对 leaf 的理解对不对。我印象中它是分,可以选择生成不同的生成方式。

点评

优点:定位准确(美团开源);说出两大核心模式(号段/雪花);有自知之明。
不足:号段模式细节缺失(双 Buffer、MySQL 表结构、本地缓存号段);雪花模式细节缺失(64 位位运算构成、时钟回拨处理);未做选型对比;未提主流分布式 ID 方案(UUID/自增/Redis/雪花/号段)。
补充:Leaf 三种模式对比——Leaf-Segment(DB + 双 Buffer + 号段)、Leaf-Snowflake(时钟+位运算+时钟回拨检测);64 位 = 1 符号 + 41 时间戳 + 10 workerId + 12 序列;时钟回拨是最大坑。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对**号段模式 + 雪花算法**两种模式的**底层机制 + 权衡 + 落地细节**的掌握。

**知识点拆解**:
1. **Leaf-Segment(号段模式)**:
- **核心思想**:从 DB 一次取一批号段(step=1000)在本地缓存,用完再取,**降低 DB 访问频率到 1/1000**
- **DB 表结构**:`Leaf_alloc(id, name, max_id, step)`,每次通过 `UPDATE ... SET max_id = max_id + step WHERE name = ?` 原子获取新号段
- **双 Buffer 机制**:一个 buffer 在前台使用、另一个在后台**异步预加载**下一批,前台 buffer 使用到阈值(如 10%)时触发后台预加载,避免"号段耗尽瞬间阻塞"
- **特点**:依赖 DB、局部单调、号段连续可读、性能高(本地生成)
2. **Leaf-Snowflake(雪花算法)**:
- **64 位位运算构成**:`1 位符号 + 41 位时间戳 + 10 位机器 ID + 12 位自增序列`
- **41 位时间戳**:约 **69 年**(从起始时间开始计)
- **10 位机器 ID**:最多 **1024 台机器**,可通过 ZooKeeper/Curator 分配 workerId,解决 workerId 冲突
- **12 位序列号**:**每毫秒 4096 个 ID**
- **单机生成、无网络调用、QPS 百万级**
- **最大坑:时钟回拨**(时间倒退导致 ID 重复)
- Leaf 方案:检测到时**抛异常并报警**,人工介入
- 其他方案:等待回拨时长、用上一机器时间戳、允许小回拨
3. **选型对比**:
| 方案 | 依赖 | 单调性 | 时钟风险 | 性能 | 场景 |
|---|---|---|---|---|---|
| UUID | 无 | 无 | 无 | 高 | 无要求 |
| DB 自增 | DB | 严格 | 无 | 低 | 单库低并发 |
| Redis INCR | Redis | 严格 | 无 | 高 | 高 QPS 简单场景 |
| 雪花算法 | 时钟 | 严格 | **有回拨** | 极高 | 高 QPS 内部 ID |
| Leaf 号段 | DB | **局部** | 无 | 高 | 业务可读(订单号) |
| Leaf-Snowflake + ZK | ZK | 严格 | **有回拨** | 极高 | 需要自动分配 workerId |

> 按「号段模式 → 雪花模式 → 选型对比」递进。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清号段模式的双 Buffer 机制、雪花算法的位运算构成
- **广度**:对比 UUID/DB/Redis/雪花/号段五种方案
- **应用**:时钟回拨的解决方案、Leaf-Segment 和 Leaf-Snowflake 的场景选择

**延伸递进面试题**:
1. **号段模式的双 Buffer 机制具体怎么工作?为什么不直接同步预加载?**
- 解答要点:前台 buffer 使用到阈值(如 10%)时触发后台异步加载下一个号段;同步加载会在号段耗尽瞬间阻塞业务,双 Buffer 保证前台永远有号段可用
2. **雪花算法时钟回拨如何检测和解决?**
- 解答要点:检测 `currentTime < lastTime` 判定回拨;Leaf 严格模式抛异常报警;宽松模式等待回拨时长;使用 TSC(时间戳计数器)绕过系统时钟;使用上一台机器的时间戳(有兼容问题)
3. **为什么美团选择 Leaf-Segment 做订单号,而不是雪花算法?**
- 解答要点:订单号需要**局部有序**(分库分表 hash 后接近有序)、**可读性**(业务方好理解)、**避免时钟回拨风险**(金融场景不能容忍 ID 重复);雪花算法每毫秒 4096 个 ID 太快,订单量对不上

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Leaf 是美团开源的分布式 ID 生成组件,主要有**两种模式**:

**第一种:Leaf-Segment 号段模式**
- **核心思想**:从 DB 一次取一批号段(比如 step=1000)到本地缓存,用完再取,DB 访问频率降低到 1/1000;
- **DB 表结构**:`Leaf_alloc(id, name, max_id, step)`,每次通过 `UPDATE ... SET max_id = max_id + step WHERE name = ?` 原子获取号段;
- **双 Buffer 机制**:一个 buffer 前台使用、一个后台异步预加载下一批,前台使用到阈值(比如 10%)触发后台预加载,避免号段耗尽瞬间阻塞;
- **特点**:局部单调、号段连续可读、依赖 DB,适合订单号等业务可读的场景。

**第二种:Leaf-Snowflake 雪花算法**
- **64 位位运算构成**:`1 位符号 + 41 位时间戳 + 10 位机器 ID + 12 位序列号`;
- 41 位时间戳约 69 年、10 位机器 ID 支持 1024 台机器(可通过 ZooKeeper 分配 workerId)、12 位序列号每毫秒 4096 个 ID;
- **单机生成、无网络调用、QPS 百万级**;
- **最大坑是时钟回拨**(时间倒退导致 ID 重复),Leaf 采用严格模式:**检测到回拨抛异常并报警,人工介入**,因为金融级场景不能容忍重复 ID。

**选型建议**:
- **业务可读、需要局部有序**(订单号)→ Leaf-Segment 号段;
- **高 QPS、内部 ID** → Leaf-Snowflake;
- **需要自动分配 workerId** → Leaf-Snowflake + ZooKeeper。

主流对比:**UUID** 无序不可读、**DB 自增** 单点瓶颈、**Redis INCR** 依赖 Redis 可用性、**雪花算法** 有回拨风险、**号段模式** 依赖 DB 但可读性好——没有银弹,按业务需求选。

#7 单机压测到 300QPS,10 台就能抗住 3000QPS 吗? medium system-design

题目

单机压测到 300QPS,10 台就能抗住 3000QPS 吗?

我的回答

Q7 是 300QPS 单台机的话,它是自身的压力上限。但是 10 台进行分布的时候,你并不能保证每一台都分配到 300,有可能一台分配到 600,其他台其他几台分配到其他的,会出现分配不均的问题。所以并不能完全信任 10 台就能扛住 3000,需要有一定的富余保留,一般在 30%、20%~30% 这样,做一个富余保留,确保机器能扛得住压力。另外在前侧做负载均衡的时候,尽可能的选择更合理的分配算法。不要,尽量不要选择完全随机的。可能选择压力低的去进行分配,或者是轮询,或者是高性能的机器选择权重轮询,有些高性能的分多点,有些低性能的分少点。但是你无法完全的保证每台的压力都是均分的,QPS 没有那么稳定。

点评

优点:核心结论"不能完全信任、需要富余"抓准;说出流量倾斜问题(一台 600、其他少);提到负载均衡算法(轮询/加权/最小压力);有容量冗余意识(20-30% 富余)。
不足:未提**长尾流量与突发**(3000 不是稳态峰值,可能是毛刺);未提**依赖资源瓶颈**(DB/缓存/第三方接口可能先挂);未提**容量规划方法论**(压测 → 峰值 → 水位 → 冗余);未量化 30% 富余背后的原因(GC/抖动/长尾请求/慢查询);未提**限流降级**兜底;未提**水平扩展**的隐性成本(一致性、锁、状态同步)。
补充:容量规划黄金公式——真实需求 = 峰值 QPS × (1 + 冗余系数) ÷ 单机稳态 QPS;一般预留 50% 冗余(非 20-30%);负载均衡策略对比:RR/加权 RR/IP Hash/一致性 Hash/最少连接/响应时间权重。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察**容量规划的量化思维**——不是"能不能扛",而是"凭什么说能扛"。

**知识点拆解**:
1. **单机 300 QPS 是"压测上限",不是"稳态容量"**:
- 压测上限是**极限水位**(如 CPU 95%、GC 频繁),不能作为规划基准
- 稳态水位应该是**CPU 50-60%**(如 150 QPS),预留响应抖动、GC、慢请求的时间
2. **流量倾斜问题**(用户提到,正确):
- **长尾请求**:P99/P999 请求耗时可能是 P50 的 100 倍,长尾会让某台机器先被打爆
- **热点数据**:某些 key/用户/商品会被集中访问,即使 RR 也无法均分
- **连接粘性**:Session/HTTP Keep-Alive 会导致连接倾斜
3. **依赖资源瓶颈**(用户遗漏):
- **数据库**:连接池、CPU、锁、I/O——DB 单点通常比应用层先崩
- **缓存**:Redis 单分片 QPS 上限、内存限制
- **下游接口**:第三方 API、消息队列、RPC 服务的容量
- **网络**:带宽、TCP 连接数、NAT 表
4. **容量规划黄金公式**:
- `机器数 = (峰值 QPS × 冗余系数) ÷ 单机稳态 QPS`
- **冗余系数**:一般 1.5-2(预留 50-100%),用户说的 20-30% 偏低
- **举例**:300 QPS 压测上限 → 稳态 150 QPS → 3000 峰值 × 1.5 = 4500 → 需 4500/150 = **30 台**,不是 10 台
5. **负载均衡策略对比**:
| 策略 | 特点 | 适用场景 |
|---|---|---|
| 轮询 RR | 均分、简单 | 机器同质 |
| 加权 RR | 按性能权重 | 机器异构 |
| IP Hash | 同 IP 固定机器 | 需要会话保持 |
| 一致性 Hash | 扩缩容只影响小部分 | 缓存集群 |
| 最少连接 | 按实时负载分配 | 请求耗时不均 |
| 响应时间权重 | 按历史响应时间 | 智能调度 |
6. **限流降级兜底**(用户遗漏):
- **限流**:Sentinel/RateLimiter 单机限流、网关限流、Redis 全局限流
- **降级**:核心接口兜底、非核心接口跳过、缓存兜底、静态兜底
- **熔断**:Hystrix/Resilience4j,下游慢就熔断

> 按「稳态 vs 上限 → 流量倾斜 → 依赖瓶颈 → 容量公式 → 兜底策略」递进。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否量化"稳态水位"和"冗余系数",说出容量规划公式
- **广度**:理解长尾流量、依赖瓶颈、限流降级等隐性风险
- **应用**:真实项目中的容量规划流程——压测 → 峰值预估 → 冗余计算 → 灰度验证

**延伸递进面试题**:
1. **3000 QPS 的峰值是怎么预估的?如果预估错了怎么办?**
- 解答要点:历史数据 × 业务增长 × 大促倍数;灰度压测验证;预案兜底(限流降级);分场景预案(正常/大促/异常)
2. **单机 300 QPS 的压测是稳态压测还是极限压测?如何判定?**
- 解答要点:极限压测看 CPU 95%、GC 频繁、响应时间暴涨;稳态压测要求 CPU 50-60%、P99 稳定;生产规划用稳态水位
3. **如果 10 台机器扛不住 3000 QPS,除了加机器还有什么方案?**
- 解答要点:读缓存、写异步、异步化、批处理、限流降级、扩容依赖(DB 分库分表)、算法优化、静态化/CDN

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,我的结论是:**10 台扛 3000 QPS 风险很大,不能直接上线**,需要按容量规划公式重新算。

**第一,单机 300 QPS 是压测上限,不是稳态容量**。压测上限是 CPU 95%、GC 频繁、响应时间暴涨时的极限水位,不能作为规划基准。**稳态水位应该是 CPU 50-60%**,也就是单机实际能稳态跑 150 QPS 左右。

**第二,流量倾斜是真实存在的**。
- **长尾请求**:P99 耗时可能是 P50 的 100 倍,长尾请求会让某台机器先被打爆;
- **热点数据**:某些 key/用户会被集中访问,即使 RR 也无法均分;
- **连接粘性**:Session/Keep-Alive 会导致连接倾斜;
- 所以即使 10 台 RR 分配,也可能出现你说的"一台 600、其他少"的情况。

**第三,容量规划黄金公式**:`机器数 = (峰值 QPS × 冗余系数) ÷ 单机稳态 QPS`。
- 冗余系数一般 **1.5-2**(预留 50-100%),你说的 20-30% 偏低;
- 按稳态 150 QPS 算:3000 × 1.5 ÷ 150 = **30 台**,不是 10 台;
- 冗余是为了应对 GC 抖动、长尾请求、慢查询、依赖资源抖动等。

**第四,依赖资源瓶颈常常先挂**。
- **DB**:连接池、CPU、锁、I/O——DB 单点通常比应用层先崩;
- **缓存**:Redis 单分片 QPS 上限;
- **下游接口**:第三方 API、消息队列容量;
- **网络**:带宽、TCP 连接数;
- 需要一起评估,不能只看应用层。

**第五,负载均衡策略选择**(我举的点):
- **RR/加权 RR**:机器同/异构时均分;
- **一致性 Hash**:扩缩容友好;
- **最少连接**:请求耗时不均时最好;
- **响应时间权重**:智能调度。

**第六,限流降级兜底**:
- **限流**:Sentinel 单机 + 网关全局 + Redis 全局限流;
- **降级**:非核心接口跳过、缓存兜底、静态兜底;
- **熔断**:Resilience4j 熔断下游慢调用。

**总结**:容量规划是"稳态水位 × 冗余系数",不是"压测上限 × 机器数"。生产实践是**压测 → 峰值预估 → 冗余计算 → 灰度验证 → 限流降级兜底**五步走,没有兜底就上线是拿业务冒险。

#8 为什么说做架构其实就是做权衡? high system-design

题目

为什么说做架构其实就是做权衡?

我的回答

架构做权衡,这个事本身就是一个很现实的问题。做架构本身是因为当前的资源不足,才要去考虑架构的。资源完全充足的情况下,可以完全大力飞砖。单台机器性能足够高,就可以选择单体去承接所有的流量。单个机器的单个的那个服务的能力足够强,这个数据库可能能承载几十万的、几百万的并发数据库,它就不需要做分布式数据库,不需要做分库分表。另外就是架构就是为了处理单台处理不了,或者是说你本身这个结构。他需要去更用更少的资源去做更多的事,才做这个架构这件事。而在执行架构的设计过程中,会考虑到方方面面的事,比如安全性、一致性、性能,然后可靠性,各种东西的权衡。比如像 CAP 原理,一致性、可用性、分区容错性,在分布式里边,这几个,这三个东西就你都无法同时拿到。你只能去权衡取舍,是考虑要强一致还是高可用,这都是需要通过架构去取舍。而这个取舍的过程其实就是架构设计。也是根据自己的业务情况去不同的选择。能接受高可用,能接受一定的不一致,就可以选,最好选高可用。接受不了不一致,就选择强一致,放弃一些高可用。权衡,一方面是对成本的权衡,一方面是对性能的权衡。取舍,各方面都会有这种取舍。

点评

优点:抓准"架构是为了应对资源不足"的本质;用 CAP 举例说明取舍;强调业务导向(能接受不一致就选 AP)。
不足:维度不够系统(缺开发效率 vs 复杂度、实时性 vs 一致性、扩展性 vs 简单性);缺方法论(如何决策);缺权衡的边界(YAGNI);缺权衡的动态性(架构会演进);缺具体案例深度。
补充:架构权衡七大维度(CAP/实时 vs 最终/性能 vs 成本/扩展性 vs 简单性/开发效率 vs 复杂度/可维护 vs 灵活/可观测 vs 侵入);四步方法论(约束条件→候选方案→多维打分→最坏情况评估);架构演进核心是"够用就行,问题驱动"。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对**架构权衡**的**系统性理解**——不是知道要权衡,而是能系统性地列出权衡维度、方法论、边界和案例。

**知识点拆解**:
1. **架构权衡的本质**:
- 架构是**业务约束下的最优解**,不是"技术最优"或"理论最优";
- 架构是为了应对**"当前方案已经不够用"**才引入的,不是主动炫技;
- **YAGNI 原则**(You Aren't Gonna Need It):不要过度设计,简单能扛就先简单扛;
- 权衡的核心是"没有银弹"——每个方案都有代价,关键是**代价是否可承受**。
2. **架构权衡七大维度**:
1. **一致性 vs 可用性**(CAP):CP 强一致(ZK、MySQL 主从同步)、AP 弱一致(Redis、Cassandra)
2. **实时性 vs 最终一致性**:同步 RPC(实时强一致)vs MQ 异步(解耦但最终一致)
3. **性能 vs 成本**:自研 vs 云服务、垂直优化 vs 水平扩展、单机优化 vs 分布式
4. **扩展性 vs 简单性**:微服务拆分 vs 单体、水平扩展 vs 提前分片
5. **开发效率 vs 复杂度**:微服务治理(注册中心、熔断、链路追踪)成本高,但利于团队扩张
6. **可维护性 vs 灵活性**:模块化强类型(可维护)vs 快速原型开发(灵活)
7. **可观测性 vs 侵入性**:链路追踪埋点(可观测)vs 业务代码被污染(侵入)
3. **权衡方法论四步**:
- **第一步:明确业务约束**——QPS 峰值、并发、SLA(P99 响应时间)、预算、团队能力、上线时间
- **第二步:列出候选方案**——至少 2-3 个(如 Redis vs MySQL vs 混合;单机 vs 分布式)
- **第三步:多维度打分对比**——性能/成本/复杂度/风险/可维护性/团队能力
- **第四步:最坏情况评估**——选错了代价是什么?能否灰度回滚?
4. **架构演进的动态性**:
- 架构是**演进**的,不是一次定死;
- 业务变化时权衡结论要**重新评估**(比如原来 1000 QPS 用单体,现在 100000 QPS 必须微服务);
- 阿里早期是单体,业务增长后逐步微服务化,**不是一开始就上微服务**——这是"够用就行"的典型;
- **反模式**:过度设计(早期就上微服务+分库分表+服务网格,团队 3 个人根本扛不住)。

> 按「本质认知 → 七大维度 → 四步方法论 → 演进动态性」递进。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否系统列出权衡维度、说出四步方法论
- **广度**:能否举出 2-3 个具体权衡案例(如 Redis 缓存 vs DB、MQ 异步 vs RPC 同步、微服务 vs 单体)
- **应用**:能否结合自己项目说清某个权衡的决策过程

**延伸递进面试题**:
1. **举一个你项目中做过的重要架构权衡,说清权衡维度和最终决策。**
- 解答要点:说清业务背景、候选方案、权衡维度、决策过程、上线效果、后续演进
2. **微服务拆分什么时候合适?什么时候应该用单体?**
- 解答要点:微服务适用于团队规模 > 10 人、服务间依赖复杂、独立部署/扩容需求强、业务领域边界清晰;单体的适用场景:早期快速迭代、团队小、依赖简单、QPS 不高
3. **如果业务突然 100 倍增长,你原来的架构决策还成立吗?**
- 解答要点:架构决策依赖当时的业务约束,业务变化后必须重新评估;关键是**架构的扩展性预留**(如设计时就考虑分片、异步化);演进路径(垂直优化 → 水平扩展 → 拆分)

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,架构做权衡,是因为**没有任何技术能同时满足所有诉求**,我们只能在业务约束下选最优解。

**架构权衡的三大前提**:
1. **架构是为了应对"当前方案已经不够用"**才引入,不是主动炫技。资源充足时单体能扛就用单体,YAGNI 原则(You Aren't Gonna Need It),别过度设计。
2. **架构是业务约束下的最优解**,不是技术最优。同样一个需求,金融业务和 C 端业务选的方案完全不一样。
3. **权衡的核心是"代价是否可承受"**——每个方案都有代价,关键是业务能不能接受。

**架构权衡七大维度**:
- **一致性 vs 可用性**(CAP):CP 选 ZK/MySQL 主从同步、AP 选 Redis/Cassandra;
- **实时性 vs 最终一致性**:同步 RPC 强一致 vs MQ 异步最终一致;
- **性能 vs 成本**:自研 vs 云服务、垂直优化 vs 水平扩展;
- **扩展性 vs 简单性**:微服务拆分 vs 单体,水平扩展 vs 提前分片;
- **开发效率 vs 复杂度**:微服务治理成本高,但利于团队扩张;
- **可维护性 vs 灵活性**:模块化强类型 vs 快速原型;
- **可观测性 vs 侵入性**:链路追踪埋点 vs 业务代码被污染。

**权衡方法论四步**:
1. **明确业务约束**:QPS 峰值、SLA、P99 响应、预算、团队能力、上线时间;
2. **列出候选方案**:至少 2-3 个(Redis vs MySQL vs 混合);
3. **多维度打分对比**:性能/成本/复杂度/风险/可维护/团队能力;
4. **最坏情况评估**:选错了代价是什么?能否灰度回滚?

**举例**——我在项目里做过一个典型权衡:早期用户查询订单,一开始用 MySQL 单表直接查,QPS 几百。业务量涨到几千 QPS,我评估三个方案:**加 Redis 缓存**(性能好但有一致性问题)、**分库分表**(复杂度高但可扩展)、**读副本**(简单但有主从延迟)。权衡结果是选**加 Redis 缓存 + 延迟双删**,因为**成本最低、开发最快、能满足 90% 场景**,同时用**业务幂等兜底**缓存不一致的极端情况。

**架构演进的核心**:不是"想清楚再做",而是"**够用就行,问题驱动演进**"。阿里早期也是单体,业务增长后逐步微服务化。过早微服务化、过早分库分表,都是拿团队成本和复杂度换不存在的扩展性。

**总结**:架构不是画图,是**在业务约束下用最少代价满足最多诉求**——权衡的本质是"没有银弹",权衡的价值是"选到最优解",权衡的边界是"够用就行"。

#9 架构是设计出来的还是演进出来的? high system-design

题目

架构是设计出来的还是演进出来的?

我的回答

我理解的架构基本上是演进出来的。你最开始的设计无法考虑到未来时代的变化,每个阶段有不同的变化。你最开始的设计可能会沿用一段时间,但时代发生了变化之后,你的架构可能就不再适用了。就比如现在这个社会,突然的 AI 的诞降临,导致大部分的原有的架构都被打破了。新的内容被填充进去,老的内容被移走。一次性的设计无法考虑到那么久远,或者时代变更,某些东西的产生,你是无法预测的。所以根据时代的变化、世界的变化去调整架构,是一个必然的过程。

点评

优点:有明确立场(演进派);核心洞察"未来不可预测"正确;用 AI 冲击举例说明技术变革对架构的影响。
不足:过度倾向"演进"、完全否定了"设计"的价值(主流观点是"设计 + 演进"结合);未对比两种观点的适用场景;未讲"如何演进"的方法论(DDD/绞杀者模式/微服务拆分);未讲什么时候必须"设计"(核心金融业务/合规/安全/一致性强约束);未讲架构师在演进中的作用(架构守护、抽象层、技术债务管理)。
补充:正确答案是"设计 + 演进"结合——**设计决定方向,演进适应变化**;关键在**抽象层设计**(预留扩展点、依赖倒置、领域建模),让架构能被"演进";不同场景选不同侧重点:核心业务/金融/合规必须设计(强约束);探索性业务/快速迭代适合演进。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对**架构设计派 vs 演进派**两种观点的辩证理解,以及能否说清"设计"与"演进"如何结合。

**知识点拆解**:
1. **两种观点的辩证**:
- **设计派**(前微软 Bruce Eckel、Martin Fowler 早期):架构需要**前瞻性设计**,尤其是核心业务、金融、合规、安全等强约束领域,前期设计不到位后期返工成本极高;
- **演进派**(Martin Fowler 中后期、Kent Beck):架构是**问题驱动演进**的,过早设计是过度设计(YAGNI),业务变化时会发现前期设计是错的;
- **正确立场**:**"设计 + 演进"结合**——设计决定方向(核心抽象、边界、扩展点),演进适应变化(业务迭代、技术选型更新)。
2. **不同场景选不同侧重点**:
| 场景 | 侧重点 | 原因 |
|---|---|---|
| 核心金融业务 | 设计派 | 强一致、合规审计、返工代价极高 |
| 支付/交易 | 设计派 | 分布式事务、幂等、对账,前期设计错后期无法挽救 |
| 探索性业务 | 演进派 | 需求不清、快速验证、迭代快 |
| 电商促销 | 演进派 | 需求多变、场景丰富,快速试错 |
| 基础设施(K8s/服务网格) | 设计派 | 技术栈稳定、长期投入 |
| 应用层业务代码 | 演进派 | 需求变化频繁、DDD 演进友好 |
3. **"设计"的价值——决定不能变的**:
- **领域边界**:DDD 战略设计(Bounded Context);
- **扩展点**:依赖倒置、策略模式、SPI 扩展;
- **抽象层**:Repository、Gateway、Adapter 等抽象;
- **数据模型**:核心实体、领域事件、幂等键;
- **技术底座**:注册中心、消息队列、缓存选型。
4. **"演进"的价值——适应变化的**:
- **业务迭代**:新功能、新场景通过策略模式/插件扩展;
- **技术升级**:JDK 版本、框架版本、中间件升级;
- **性能优化**:从同步到异步、从单库到分库分表、从 MySQL 到 ES;
- **团队扩张**:从单体到微服务的自然演进。
5. **演进的关键——可演进性的设计**:
- **依赖倒置**:核心业务不直接依赖实现;
- **接口抽象**:关键扩展点用接口定义;
- **DDD 领域建模**:领域边界清晰,跨边界通过事件解耦;
- **绞杀者模式(Strangler Fig Pattern)**:微服务迁移时用新服务逐步替换老服务;
- **CQRS + Event Sourcing**:读写分离、事件溯源,便于演进;
- **可观测性**:埋点、链路追踪、指标监控,让演进有数据支撑;
- **架构守护**:ArchUnit、依赖约束、代码审查,防止演进变腐化。
6. **反面案例**:
- **过度设计**:早期就上微服务+服务网格+分库分表,团队 3 个人扛不住(比如某些"技术炫技"项目);
- **零设计**:早期完全无规划,堆屎山代码,后期重构成本 > 重写成本。

> 按「辩证立场 → 场景选择 → 设计价值 → 演进价值 → 演进方法」递进。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否辩证理解"设计 + 演进"结合,而不是二元对立
- **广度**:能否说清演进的方法论(DDD/绞杀者/CQRS/依赖倒置)
- **应用**:能否结合自己项目说清"哪些地方必须设计、哪些地方适合演进"

**延伸递进面试题**:
1. **举一个你项目中"演进"的典型案例,说清演进路径。**
- 解答要点:说清初始架构、演进驱动力、演进路径、每一步的权衡、最终形态
2. **绞杀者模式(Strangler Fig Pattern)具体如何工作?**
- 解答要点:用新服务逐步替换老服务的功能模块,通过 API Gateway 路由切流,新老共存 → 逐步替换 → 老服务下线;适用于单体→微服务的迁移
3. **架构守护(Architecture Guard)如何做?如何防止"演进变腐化"?**
- 解答要点:ArchUnit 单元测试、依赖约束(dependency-cruiser)、代码审查、CODEOWNERS、静态分析、架构决策记录(ADR)

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,我的观点是:**架构是"设计 + 演进"的结合**——设计决定方向,演进适应变化,两者缺一不可。

**先说设计的价值**:设计不是"一次性做完",而是**决定那些不能变的**——
- **领域边界**:DDD 战略设计(Bounded Context)划清服务/模块边界,避免后期耦合严重;
- **扩展点**:依赖倒置、策略模式、SPI,为未来变化预留口子;
- **抽象层**:Repository、Gateway 等抽象隔离实现,实现可替换;
- **数据模型**:核心实体、领域事件、幂等键,跨系统一致的关键;
- **技术底座**:注册中心、MQ、缓存选型,一旦选定很难改。

这些前期不设计好,后期返工成本极高,尤其是**金融、支付、合规**这类强约束业务,架构错一次可能就要重写。

**再说演进的价值**:演进是**适应业务变化**的必然——
- 业务场景持续变化(新功能、新客群);
- 技术栈持续升级(JDK 版本、框架版本);
- 性能需求变化(从同步到异步、单库到分库分表);
- 团队扩张(从 3 人到 30 人,从单体到微服务);
- 就像你说的 AI 冲击,新技术会打破旧架构,必须演进。

**关键是"可演进性的设计"**——设计要让架构能被演进,而不是设计成"死板"的。具体做法:
- **DDD 领域建模**:领域边界清晰,跨边界通过事件解耦;
- **依赖倒置**:核心业务不依赖具体实现;
- **绞杀者模式**:微服务迁移时新老共存、逐步替换;
- **CQRS + Event Sourcing**:读写分离、事件溯源;
- **架构守护**:ArchUnit 单元测试、依赖约束、架构决策记录(ADR),防止演进变腐化。

**结合项目举例**:我做过一个订单系统,早期用**单体 + MySQL 单表**(快速上线),用户量涨到 1000w 时**加 Redis 缓存**(应对读压力),涨到 5000w 时**读写分离 + 主从复制**,涨到 1 亿时**分库分表**(按 user_id hash),最后业务边界清晰了再**拆分成订单中心、支付中心、库存中心**(微服务)。整个演进路径都是"够用就行,问题驱动",早期不上微服务是因为团队只有 3 个人。

**反面案例**:过度设计(早期就上微服务+服务网格+分库分表)会导致团队扛不住、复杂度爆炸;零设计会导致后期堆屎山、重构成本 > 重写成本。所以是**"设计 + 演进"结合**——**核心业务必须设计、应用层适合演进**,找到平衡点才是关键。

**总结**:架构不是"设计出来就完事",也不是"完全靠演进",而是"**设计让演进有方向,演进让设计不僵化**"。好的架构师既能**前期设计好关键抽象**,又能**后期演进好业务变化**——这才是 TL 的核心能力。

#10 服务端接口性能优化有哪些方案? high system-design

题目

服务端接口性能优化有哪些方案?

我的回答

服务端接口性能优化。首先你需要考虑接口需要优化吗?这个接口性能存在问题吗?它使用的场景是什么样子?确认它需要达到什么样的指标?确认完这些之后,才去考虑优化性能的方案。 常规的包括,如果可以异步,首先是异步去处理这些里边耗时的东西,然后加缓存,如果能接受一定时间的不一致,就加缓存。这个数据存在于缓存,与去从数据库中拿,效率肯定是不一样的。然后如果对缓存的并发量还是有要求的话,可能远程缓存不行,还需要加二层的本地缓存。降低远程访问的使用。数据库层面的优化包括可以做数据库的拆分,数据库 SQL 的优化。基本上是这些方案。 细化下,从上面,最上面应该是网络层,就近访问。设置就近的资源中心服务器,就近访问。然后中间层到数据分流,不要把所有的流量打到一个服务器上,而是做好均分,均分到不同的服务器。然后再往下是接口层面的,是在接口层增加缓存。增加缓存可能是,如果流量足够高,可能要加本地缓存。然后二级的本地缓存,一级的远程缓存。然后都防没有,然后再去打数据库。然后数据库层面的优化是,首先是 SQL 本身的优化,性能优化,索引。使用覆盖索引,降低数量返回等等。然后如果是三方调用,三方调用的话做异步,然后异步的然后进行数据在组侧进行组装,将多个服务的数据进行组装,异步的进行组装。然后是数据库层面还可以进行,如果数据量比较大,做分库分表,这些操作,或者说选择不同的数据库介质。比如,如果关系型数据库不行,是否可以用像 ClickHouse、ES 这些大的数据库模型去进行数据库处理。

点评

优点:前置判断极佳(先定位瓶颈再优化);分层清晰(网络→负载均衡→接口→缓存→数据库);方案具体(异步/多级缓存/覆盖索引/分库分表/换技术栈/异步组装);有工程思维(业务权衡、就近访问、扩展性)。
不足:遗漏代码级优化(N+1 查询、批处理、并行流);遗漏基础设施调优(JVM/GC、线程池、连接池);遗漏可观测性(链路追踪、性能监控);遗漏兜底策略(限流/熔断/降级/预热);遗漏传输层(gzip/HTTP2/gRPC/CDN)。
补充:性能优化五步法——定位瓶颈(链路追踪+火焰图)→ 代码优化(N+1/批处理/并行)→ 缓存优化(多级/预热/穿透防护)→ 基础设施(JVM/线程池/连接池)→ 架构演进(分库分表/CQRS/读写分离);不同数据库适用场景:MySQL 事务/ClickHouse OLAP/ES 搜索/HBase 海量 KV。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察**性能优化的系统性思维**——不是罗列方案,而是能系统分层、量化目标、兜底保护。

**知识点拆解**:
1. **前置判断(用户抓准了这个)**:
- **先定位瓶颈再优化**——不要盲目改代码,先看链路追踪和监控指标
- **明确业务指标**:QPS、P99 响应时间、并发、可用率
- **明确优化目标**:优化后能达到什么指标?(如 P99 从 500ms 降到 100ms)
- **权衡成本**:优化方案的成本 vs 收益,避免过度优化
2. **五步法**(覆盖用户提到的所有维度 + 补充遗漏项):

**Step 1:定位瓶颈**
- 链路追踪(SkyWalking/Jaeger/Zipkin)
- 火焰图(Flame Graph)看 CPU 热点
- 慢查询日志、慢接口日志
- APM 工具(Datadog/Prometheus+Grafana)

**Step 2:代码级优化**(用户遗漏)
- **N+1 查询**:ORM 常见坑,用 `JOIN` 或 `foreach` 批处理代替
- **批处理**:批量插入/批量查询代替循环单条(`INSERT ... VALUES (...), (...), (...)`)
- **并行调用**:`CompletableFuture.supplyAsync` 并行调多个下游服务
- **异步化**:耗时操作扔 MQ,主流程立即返回
- **本地计算**:减少远程调用,把能算的算在本地

**Step 3:缓存优化**(用户提到)
- **多级缓存**:本地缓存(Caffeine)+ 远程缓存(Redis)
- **缓存预热**:服务启动时预加载热点数据
- **缓存穿透防护**:布隆过滤器 / 空值缓存
- **缓存击穿防护**:互斥锁 / 逻辑过期
- **缓存雪崩防护**:过期时间加随机数

**Step 4:基础设施调优**(用户遗漏)
- **JVM 调优**:G1/ZGC 收集器、堆大小、元空间、直接内存
- **线程池**:核心线程数、队列策略(CallerRuns vs Abort)、拒绝策略
- **连接池**:HikariCP(DB)、OkHttp/Apache HC(HTTP)、Dubbo/Feign
- **OS 参数**:`somaxconn`、`tcp_tw_reuse`、`file-max`

**Step 5:架构演进**(用户提到)
- **读写分离**:主从复制、CQRS
- **分库分表**:水平分片、按 user_id/hash 分片
- **异步化架构**:主流程同步 + 副作用异步(消息队列)
- **换技术栈**:MySQL(事务)→ ClickHouse(OLAP 分析)→ ES(搜索)→ HBase(海量 KV)
- **CDN 静态化**:前端资源、图片、静态页面走 CDN

3. **兜底策略(用户遗漏,非常重要)**:
- **限流**:Sentinel 单机限流 + 网关全局限流 + Redis 全局限流
- **熔断**:Resilience4j/Hystrix,下游慢就熔断,防止雪崩
- **降级**:非核心接口跳过、缓存兜底、静态兜底、异步兜底
- **预热**:服务预热(缓存、JIT)、灰度发布(1% → 50% → 100%)
- **压测**:JMeter/wrk/ab,量化优化前后对比(不量化 = 没优化)

4. **不同数据库适用场景**(用户提到但未展开):
| 数据库 | 适用场景 |
|---|---|
| MySQL | 事务、强一致、中等数据量 |
| ClickHouse | OLAP 分析、海量聚合查询、列存 |
| Elasticsearch | 全文搜索、复杂查询、日志 |
| HBase | 宽表、海量 KV、非结构化 |
| Redis | 缓存、实时排行、分布式锁 |
| MongoDB | 文档型、灵活 Schema |

> 按「前置判断 → 五步法 → 兜底策略 → 技术选型」递进。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说出 N+1 查询、并行调用、连接池调优等具体细节
- **广度**:能否覆盖代码/缓存/JVM/线程池/架构五个维度
- **应用**:能否结合自己项目说清优化前后的量化对比

**延伸递进面试题**:
1. **如何定位一个慢接口的瓶颈?给一个具体的排查流程。**
- 解答要点:先看 APM 指标(RT/QPS/错误率)→ 链路追踪看哪个 span 慢 → 慢 SQL 日志 → 火焰图看 CPU 热点 → 数据库慢查询 → 分场景分析(GC、IO、锁竞争)
2. **CompletableFuture 并行调用多个下游服务,需要注意什么?**
- 解答要点:用独立线程池(不要用 ForkJoinPool.commonPool 会互相阻塞);超时控制(`orTimeout`);异常兜底(`exceptionally`);结果合并(`thenCombine`)
3. **压测如何避免影响线上用户?**
- 解答要点:影子表(压测数据隔离);标记流量(请求头 X-Shadow-Request);错峰压测(凌晨);限流保护(压测流量单独限流);灰度压测(先 1% 用户)

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,性能优化我会按**五步法**来:

**Step 0:前置判断**——优化前必须**先定位瓶颈**,不要盲改代码。要看**链路追踪**(SkyWalking/Jaeger)看哪个 span 慢,看**火焰图**看 CPU 热点,看**慢 SQL 日志**、**APM 指标**(RT/QPS/错误率)。同时明确业务指标(QPS/P99 响应/并发)和优化目标(如 P99 从 500ms 降到 100ms),避免过度优化。

**Step 1:代码级优化**——
- **N+1 查询**:ORM 常见坑,主查询触发 N 次子查询,用 `JOIN` 或 `foreach` 批处理代替;
- **批处理**:批量插入/批量查询代替循环单条;
- **并行调用**:`CompletableFuture.supplyAsync` 并行调多个下游服务,注意用独立线程池(不要用 commonPool)+ 超时 + 异常兜底;
- **异步化**:耗时操作扔 MQ,主流程立即返回;
- **本地计算**:减少远程调用,能算的算在本地。

**Step 2:缓存优化**——
- **多级缓存**:Caffeine 本地 + Redis 远程,减少网络开销;
- **缓存预热**:启动时预加载热点数据;
- **穿透/击穿/雪崩防护**:布隆过滤器、互斥锁、过期时间加随机数;
- **业务权衡**:能接受一定不一致就缓存,实时性要求高就不缓存或短过期。

**Step 3:基础设施调优**——
- **JVM**:G1/ZGC 收集器、堆大小、元空间、直接内存;
- **线程池**:核心线程数、队列策略、拒绝策略;
- **连接池**:HikariCP(DB)、OkHttp/Apache HC(HTTP);
- **OS 参数**:`somaxconn`、`tcp_tw_reuse`、`file-max`。

**Step 4:架构演进**——
- **读写分离**:主从复制、CQRS;
- **分库分表**:水平分片,按 user_id/hash 分片;
- **异步化架构**:主流程同步 + 副作用异步;
- **换技术栈**:MySQL(事务)→ ClickHouse(OLAP 分析)→ ES(搜索)→ HBase(海量 KV);
- **CDN 静态化**:前端资源、图片走 CDN。

**Step 5:兜底策略**——
- **限流**:Sentinel 单机 + 网关全局 + Redis 全局限流;
- **熔断**:Resilience4j,下游慢就熔断防雪崩;
- **降级**:非核心接口跳过、缓存兜底、静态兜底;
- **预热**:服务预热、灰度发布(1% → 50% → 100%)。

**举例**——我在项目里做过一个订单查询接口优化,一开始 P99 是 800ms:
1. **定位**:链路追踪发现是 N+1 查询(一次查订单触发 100+ 次查商品);
2. **优化**:改成 `JOIN` + 本地缓存商品数据;
3. **调优**:调大 HikariCP 连接池、G1 GC 调参;
4. **兜底**:加 Redis 缓存兜底、Sentinel 限流;
5. **压测**:JMeter 压测前后 P99 从 800ms → 120ms,QPS 从 500 → 3000。

**总结**:性能优化不是"堆代码",而是**"先定位 → 再优化 → 后兜底 → 量化验证"**的四步闭环。核心是**权衡成本 vs 收益**——不是所有慢接口都值得优化,先明确业务指标再动手。

📚 查看全部面试记录