知

模块五 · 架构与表达

第 9 / 11 节20 分钟阅读型

架构与项目:从写接口到做系统

架构题考的是边界与取舍,不是名词。这一节讲分层与领域模型、接口设计、高可用的四件套,以及怎么把项目讲成判断。

读完这一节,你能做到

  1. 说清 Controller/Service/Repository 的职责边界。
  2. 判断什么时候该用贫血模型、什么时候该用充血模型。
  3. 设计幂等、可版本化、错误码统一的接口。
  4. 用限流、熔断、降级、隔离解释高可用方案。

一、分层与边界

Controller(协议适配)→ Service(业务编排)→ Repository(持久化)

三层各自只做一件事:

  • Controller 只负责参数校验、协议转换、错误码映射,不写业务。
  • Service 负责业务规则与事务边界,不关心 HTTP。
  • Repository 负责数据访问,不暴露 SQL 细节给上层。

常见错误是把业务写在 Controller 里,或让 Service 直接拼 SQL。边界一乱,测试与复用都会变难。

二、领域模型:贫血与充血

模型特点适用
贫血模型实体只有字段,逻辑在 Service简单 CRUD、团队熟悉
充血模型实体带行为,Service 只编排规则复杂、状态机多

不是充血一定更好:规则简单时,贫血模型更直接;规则复杂时,把行为放进实体能避免 Service 膨胀。

DDD 的精简用法:先划限界上下文,再定聚合根与不变式;不要一上来就堆 Repository、Domain Service、DTO 转换。

三、接口设计

四个要点:

  1. 幂等:创建类接口用唯一键或令牌;更新类接口用版本号。
  2. 版本:URL 版本(/v1)或请求头版本;不要破坏已有字段语义。
  3. 错误码:HTTP 状态码表达协议层,业务码表达业务原因;两者都要有。
  4. 分页:统一 page/size 或游标;限制最大 size,避免拖垮数据库。

返回结构保持一致(code/message/data),前端才能统一处理;不要有的接口返回数组、有的返回对象。

四、高可用四件套

手段作用关键参数
限流保护系统不被压垮QPS、并发数、令牌桶
熔断下游故障时快速失败错误率阈值、半开探测
降级牺牲非核心功能保核心兜底数据、开关
隔离防止故障扩散线程池、信号量、集群

超时是基础:没有超时,线程会被慢调用拖死。重试要配合退避与幂等,否则会放大故障。

五、项目案例:用 STAR 讲架构

S:业务规模、团队、约束(QPS、数据量、合规)
T:要解决的问题(稳定性、性能、成本)
A:方案与取舍(为什么选 A 不选 B)
R:指标变化与代价

示例:订单服务 QPS 从 2k 涨到 8k,数据库连接被打满。方案:读写分离 + 热点缓存 + 异步化下单;代价是缓存一致性复杂度上升。结果:P99 从 800ms 降到 180ms。

架构题最忌只列名词,要讲清"约束是什么、为什么这么选、付出了什么代价"。

六、常见坑

  1. Controller 里写事务与业务逻辑。
  2. 所有表都做 DTO 转换,代码量爆炸。
  3. 接口没有幂等,重试导致重复下单。
  4. 熔断阈值拍脑袋,误杀正常流量。
  5. 项目案例只讲技术栈,不讲指标与取舍。

七、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 画一个你项目的分层图,标出每层的输入输出。
  2. 给一个创建接口加幂等(唯一键 + 状态机)。
  3. 用 Resilience4j 或 Sentinel 配一次限流与熔断。

② 让 AI 写同一题

把第 2 题(接口设计)交给 AI,补一句 要有限流、幂等与降级,并说明各自放在哪一层。

③ 对比要盯的点

  • 限流算法它选的是计数器还是令牌桶?突刺流量下表现完全不同。
  • 幂等 key 存在哪、多久过期?它给了吗?
  • 降级策略它是“返回空”还是“返回上次缓存”?两者的产品语义不同。

④ 一句话取舍

它的方案在依赖方整体挂掉十分钟时,会把你的服务一起拖死吗?

八、自测

  1. 三层的职责边界是什么?
  2. 贫血与充血模型怎么选?
  3. 接口幂等的三种做法?
  4. 限流、熔断、降级、隔离分别解决什么?
  5. 架构案例应该包含哪些要素?

参考答案要点:协议/业务/持久化;规则复杂度;唯一键/令牌/版本号;保护/快速失败/兜底/防扩散;约束、方案、代价、指标。

附录:高频追问

问:Service 之间可以互相调用吗? 可以,但要避免环;跨领域用事件或防腐层解耦。

问:DTO、VO、Entity 要分吗? 边界清晰时分层有价值;小项目可适度合并,别为了分层而分层。

问:接口版本怎么兼容? 新增字段保持向后兼容,删除/改语义要走新版本;用灰度验证。

问:限流用令牌桶还是漏桶? 令牌桶允许突发,漏桶输出平滑;按业务对突发的容忍度选择。

问:熔断和降级的区别? 熔断是"不再调用",降级是"调用失败后的替代方案";通常一起用。

问:线程池隔离和信号量隔离怎么选? 线程池隔离能异步与超时,成本高;信号量轻量,但不能异步。

问:分布式系统为什么需要超时? 没有超时,故障会沿着调用链扩散,最终拖垮整个系统。

问:如何做容量评估? 从峰值 QPS、单请求耗时、资源用量反推实例数,再留 30%-50% 余量。

问:架构评审看什么? 边界、失败路径、数据一致性、可观测性、成本。

问:什么时候不该上微服务? 团队小、边界不清、运维能力不足时,单体更合适。

单体到微服务的演进路径

  1. 先做模块化单体:按业务域分模块,禁止跨模块直接访问数据库。
  2. 把最独立、最需要扩容的模块拆出去(如消息、搜索)。
  3. 补齐网关、配置、注册、链路追踪、日志聚合。
  4. 再按业务域逐步拆分,每次拆分都要能独立发布与回滚。

拆分的判断标准不是“服务小”,而是“能否独立变更、独立扩容、独立故障”。

接口幂等的实现细节

唯一键方案要处理“重复请求返回什么”:第一次返回成功结果,后续返回同一结果或“已处理”。令牌方案要保证令牌一次性消费,用 Redis 原子删除。版本号方案适合更新,冲突时返回 409。

分层的一个反例

Controller 直接调用 Repository,跳过 Service:查询类接口看似省事,但事务、权限、缓存都没有落点,后续加规则只能散在各个 Controller 里。

分层不是形式,是给横切逻辑留位置。

补遗:安全

认证(你是谁)与授权(你能做什么)要分开。常见方案:Session/Cookie、JWT、OAuth2/OIDC。JWT 无状态但要处理过期、刷新、撤销;不要把敏感信息放 payload(只是签名,不是加密)。

OWASP 高频:SQL 注入(参数化查询/预编译)、XSS(输出转义、CSP)、CSRF(SameSite、Token)、越权(资源级校验)、敏感数据泄露(加密与脱敏)。安全要放在网关与业务两层,不能只靠前端隐藏按钮。

补遗:测试与 CI

单元测试(JUnit 5 + Mockito)测逻辑,集成测试(Spring Boot Test + Testcontainers)测管道与数据库,契约测试保证接口兼容。CI 流水线:编译 → 单测 → 静态检查 → 构建镜像 → 集成测试 → 部署预发 → 灰度。覆盖率不是目标,关键路径覆盖才有意义。

补遗:常用设计模式

单例(Spring Bean 默认)、工厂(BeanFactory)、策略(支付/优惠)、模板方法(JdbcTemplate)、责任链(Filter/Interceptor)、代理(AOP)、观察者(事件)、建造者(复杂对象)。

面试不要背定义,讲“它解决了什么变化”:策略消除 if-else,责任链解耦处理顺序,代理分离横切逻辑。Spring 里几乎每个模式都能找到落点。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解