模块五 · 架构与表达
架构与项目:从写接口到做系统
架构题考的是边界与取舍,不是名词。这一节讲分层与领域模型、接口设计、高可用的四件套,以及怎么把项目讲成判断。
读完这一节,你能做到
- 说清 Controller/Service/Repository 的职责边界。
- 判断什么时候该用贫血模型、什么时候该用充血模型。
- 设计幂等、可版本化、错误码统一的接口。
- 用限流、熔断、降级、隔离解释高可用方案。
一、分层与边界
Controller(协议适配)→ Service(业务编排)→ Repository(持久化)
三层各自只做一件事:
- Controller 只负责参数校验、协议转换、错误码映射,不写业务。
- Service 负责业务规则与事务边界,不关心 HTTP。
- Repository 负责数据访问,不暴露 SQL 细节给上层。
常见错误是把业务写在 Controller 里,或让 Service 直接拼 SQL。边界一乱,测试与复用都会变难。
二、领域模型:贫血与充血
| 模型 | 特点 | 适用 |
|---|---|---|
| 贫血模型 | 实体只有字段,逻辑在 Service | 简单 CRUD、团队熟悉 |
| 充血模型 | 实体带行为,Service 只编排 | 规则复杂、状态机多 |
不是充血一定更好:规则简单时,贫血模型更直接;规则复杂时,把行为放进实体能避免 Service 膨胀。
DDD 的精简用法:先划限界上下文,再定聚合根与不变式;不要一上来就堆 Repository、Domain Service、DTO 转换。
三、接口设计
四个要点:
- 幂等:创建类接口用唯一键或令牌;更新类接口用版本号。
- 版本:URL 版本(
/v1)或请求头版本;不要破坏已有字段语义。 - 错误码:HTTP 状态码表达协议层,业务码表达业务原因;两者都要有。
- 分页:统一
page/size或游标;限制最大 size,避免拖垮数据库。
返回结构保持一致(code/message/data),前端才能统一处理;不要有的接口返回数组、有的返回对象。
四、高可用四件套
| 手段 | 作用 | 关键参数 |
|---|---|---|
| 限流 | 保护系统不被压垮 | QPS、并发数、令牌桶 |
| 熔断 | 下游故障时快速失败 | 错误率阈值、半开探测 |
| 降级 | 牺牲非核心功能保核心 | 兜底数据、开关 |
| 隔离 | 防止故障扩散 | 线程池、信号量、集群 |
超时是基础:没有超时,线程会被慢调用拖死。重试要配合退避与幂等,否则会放大故障。
五、项目案例:用 STAR 讲架构
S:业务规模、团队、约束(QPS、数据量、合规)
T:要解决的问题(稳定性、性能、成本)
A:方案与取舍(为什么选 A 不选 B)
R:指标变化与代价
示例:订单服务 QPS 从 2k 涨到 8k,数据库连接被打满。方案:读写分离 + 热点缓存 + 异步化下单;代价是缓存一致性复杂度上升。结果:P99 从 800ms 降到 180ms。
架构题最忌只列名词,要讲清"约束是什么、为什么这么选、付出了什么代价"。
六、常见坑
- Controller 里写事务与业务逻辑。
- 所有表都做 DTO 转换,代码量爆炸。
- 接口没有幂等,重试导致重复下单。
- 熔断阈值拍脑袋,误杀正常流量。
- 项目案例只讲技术栈,不讲指标与取舍。
七、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 画一个你项目的分层图,标出每层的输入输出。
- 给一个创建接口加幂等(唯一键 + 状态机)。
- 用 Resilience4j 或 Sentinel 配一次限流与熔断。
② 让 AI 写同一题
把第 2 题(接口设计)交给 AI,补一句 要有限流、幂等与降级,并说明各自放在哪一层。
③ 对比要盯的点
- 限流算法它选的是计数器还是令牌桶?突刺流量下表现完全不同。
- 幂等 key 存在哪、多久过期?它给了吗?
- 降级策略它是“返回空”还是“返回上次缓存”?两者的产品语义不同。
④ 一句话取舍
它的方案在依赖方整体挂掉十分钟时,会把你的服务一起拖死吗?
八、自测
- 三层的职责边界是什么?
- 贫血与充血模型怎么选?
- 接口幂等的三种做法?
- 限流、熔断、降级、隔离分别解决什么?
- 架构案例应该包含哪些要素?
参考答案要点:协议/业务/持久化;规则复杂度;唯一键/令牌/版本号;保护/快速失败/兜底/防扩散;约束、方案、代价、指标。
附录:高频追问
问:Service 之间可以互相调用吗? 可以,但要避免环;跨领域用事件或防腐层解耦。
问:DTO、VO、Entity 要分吗? 边界清晰时分层有价值;小项目可适度合并,别为了分层而分层。
问:接口版本怎么兼容? 新增字段保持向后兼容,删除/改语义要走新版本;用灰度验证。
问:限流用令牌桶还是漏桶? 令牌桶允许突发,漏桶输出平滑;按业务对突发的容忍度选择。
问:熔断和降级的区别? 熔断是"不再调用",降级是"调用失败后的替代方案";通常一起用。
问:线程池隔离和信号量隔离怎么选? 线程池隔离能异步与超时,成本高;信号量轻量,但不能异步。
问:分布式系统为什么需要超时? 没有超时,故障会沿着调用链扩散,最终拖垮整个系统。
问:如何做容量评估? 从峰值 QPS、单请求耗时、资源用量反推实例数,再留 30%-50% 余量。
问:架构评审看什么? 边界、失败路径、数据一致性、可观测性、成本。
问:什么时候不该上微服务? 团队小、边界不清、运维能力不足时,单体更合适。
单体到微服务的演进路径
- 先做模块化单体:按业务域分模块,禁止跨模块直接访问数据库。
- 把最独立、最需要扩容的模块拆出去(如消息、搜索)。
- 补齐网关、配置、注册、链路追踪、日志聚合。
- 再按业务域逐步拆分,每次拆分都要能独立发布与回滚。
拆分的判断标准不是“服务小”,而是“能否独立变更、独立扩容、独立故障”。
接口幂等的实现细节
唯一键方案要处理“重复请求返回什么”:第一次返回成功结果,后续返回同一结果或“已处理”。令牌方案要保证令牌一次性消费,用 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 里几乎每个模式都能找到落点。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
- ★★★★☆ 常见的限流算法有哪些?在 Java 服务里怎么落地?
- ★★★☆☆ 设计一个高可用服务,你会考虑哪些点?
- ★★★★☆ 限流、降级、熔断分别解决什么问题?怎么配合?
- ★★★★☆ 秒杀系统怎么设计?
- ★★★★☆ 线上 CPU 打满或接口突然变慢,你怎么排查?
提示:读完可以顺手做几道对应题目,检验是否真的理解