软件架构演进
核心问题:软件架构经历了哪些演进阶段?每种架构解决了什么问题、引入了什么新问题?如何根据业务阶段选择合适的架构?
1. 它解决了什么问题?
架构不是一成不变的,而是随着业务规模、团队规模、技术能力的变化而演进的。理解架构演进的脉络,能帮助你:
- 在正确的时间点做出正确的架构决策(不过度设计,也不欠设计)
- 理解每种架构的适用场景和局限性
- 避免"为了微服务而微服务"的常见陷阱
2. 架构演进全景
3. 各阶段架构详解
3.1 单体架构(Monolith)
所有功能模块打包在一个应用中,共享一个数据库,一次部署。
┌─────────────────────────────────┐
│ 单体应用 │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │用户 │ │订单 │ │商品 │ │
│ │模块 │ │模块 │ │模块 │ │
│ └──┬──┘ └──┬──┘ └──┬──┘ │
│ └───────┼───────┘ │
│ ┌──┴──┐ │
│ │ DB │ │
│ └─────┘ │
└─────────────────────────────────┘| 优势 | 劣势 |
|---|---|
| 开发简单,IDE 直接调试 | 统一部署,单个部署单元发布影响范围大 |
| 部署简单,一个 JAR/WAR | 难以按模块独立扩缩容 |
| 事务简单,本地事务即可 | 代码量增大后编译与启动可能变慢 |
| 适合快速验证业务 | 同一部署单元需共享兼容运行环境,混合技术栈成本高 |
注意:代码耦合、启动缓慢属于实现质量与代码规模问题,并非单体架构的必然属性。设计良好的模块化单体同样能做到高内聚、低耦合,表中所列为常见风险而非必然缺陷。
适用场景:初创项目、MVP 验证、团队 1-5 人、业务逻辑简单。
3.2 垂直拆分架构
按业务线将单体拆分为多个独立应用,各应用通常拥有自己的数据库。
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 用户系统 │ │ 订单系统 │ │ 商品系统 │
│ │ │ │ │ │
│ ┌────┐ │ │ ┌────┐ │ │ ┌────┐ │
│ │ DB │ │ │ │ DB │ │ │ │ DB │ │
│ └────┘ │ │ └────┘ │ │ └────┘ │
└──────────┘ └──────────┘ └──────────┘| 优势 | 劣势 |
|---|---|
| 各业务线独立开发部署 | 公共功能重复建设(如用户认证) |
| 故障隔离,一个系统挂了不影响其他 | 系统间调用方式不统一 |
| 可以按业务线独立扩展 | 数据冗余,跨系统查询困难 |
适用场景:业务线明确分离、团队 5-15 人、各业务线相对独立。
说明:垂直拆分的核心是按业务线划分应用,数据库是否物理独立需结合数据所有权与一致性需求判断,物理独立库与数据所有权边界并不完全等价。
3.3 SOA(面向服务架构)
将公共能力抽取为独立服务,通过 ESB(企业服务总线)统一通信。
| 优势 | 劣势 |
|---|---|
| 服务复用,避免重复建设 | ESB 是单点瓶颈和故障点 |
| 统一的服务治理 | ESB 配置复杂,学习成本高 |
| 支持异构系统集成 | 服务粒度粗,仍然偏重量级 |
适用场景:大型企业内部系统集成、异构系统互通。
说明:SOA 与微服务的差异不能简单归结为有无 ESB,二者在服务粒度、治理方式、自治程度和组织协作方式上都有区别,不存在绝对的技术边界。
3.4 微服务架构(Microservices)
每个服务围绕一个业务能力构建,独立开发、部署、扩展,服务间通过轻量级协议(HTTP/gRPC)通信。
| 优势 | 劣势 |
|---|---|
| 独立部署,快速迭代 | 分布式事务复杂 |
| 按需扩展,资源利用率高 | 服务间通信有网络开销 |
| 技术栈自由,各服务可选不同语言 | 需要完善的基础设施(注册发现、链路追踪等) |
| 故障隔离,熔断降级 | 运维复杂度高,需要 DevOps 能力 |
适用场景:大型系统、多团队协作(50+ 人)、需要独立扩展和快速迭代。
说明:微服务拆分以业务边界和独立演进需求为依据,服务拥有数据的逻辑所有权,但不必然意味着每个服务必须使用独立的数据库产品或服务器。
与 Spring 微服务文档的分工:本文聚焦架构理论和选型决策,具体的 Spring Cloud 组件原理和落地实践请参考 服务调用 和 服务容错。
3.5 云原生与 Serverless
维度说明:云原生技术主要解决应用的部署、编排、弹性与运行治理问题,并不要求采用特定的应用架构。单体、SOA、微服务都可以运行在容器化平台上,Serverless 则提供另一种运行与资源管理模型,是否采用取决于工作负载特征与平台约束。本节与前面各节并非单向演进关系,而是不同维度的技术选型。
| 特性 | 说明 |
|---|---|
| 容器化 | 应用打包为容器镜像,环境一致性,启动快(秒级,取决于镜像大小与运行时) |
| Kubernetes | 自动化部署、扩缩容、自愈,声明式管理 |
| 服务网格 | 可将部分服务间通信治理能力(熔断、限流、重试)下沉到基础设施层,减少业务代码对部分通信治理机制的直接依赖 |
| Serverless | 按调用次数、执行时长或资源配置等方式计费,无需管理服务器,弹性好(取决于平台与工作负载) |
4. 架构选型决策框架
4.1 选型决策树
4.2 关键决策因素
| 决策因素 | 倾向单体 | 倾向微服务 |
|---|---|---|
| 团队规模(参考) | 小团队(如 < 10 人) | 多团队(如 > 10 人) |
| 业务复杂度 | CRUD 为主 | 复杂业务逻辑,多业务域 |
| 发布频率 | 每周/每月 | 每天多次 |
| 扩展需求 | 各模块负载均匀 | 各模块负载差异大 |
| 技术栈 | 统一技术栈 | 需要混合技术栈 |
| 基础设施 | 无 K8s/DevOps | 有完善的 CI/CD 和监控 |
核心原则:先模块化单体,按需拆分。默认先建立边界清晰的模块化单体,当某个业务边界需要独立发布、独立扩缩容、故障隔离,且团队具备相应治理能力时,再评估将其拆分为独立服务。团队规模是决策因素之一,不是充分条件。不要一开始就用微服务,微服务需要服务注册发现、分布式追踪、分布式事务等基础设施,运维复杂度极高。初创项目业务不稳定,过早拆分会导致频繁的跨服务重构。
5. 分层架构模式
无论选择哪种部署架构,应用内部的分层设计都很重要。
5.1 传统三层架构
Controller(表现层)→ Service(业务层)→ DAO(数据层)问题:一种常见的退化方式是业务逻辑全部堆在 Service 层,Entity 只有 getter/setter(贫血模型),Service 越来越臃肿。这是常见实现问题,并非三层架构的必然结果。
5.2 六边形架构(端口与适配器)
核心思想:业务逻辑在中心,通过端口(接口)与外部交互,适配器负责技术细节。更换数据库、消息队列等只需替换适配器,不影响核心业务。
5.3 整洁架构(Clean Architecture)
外层(框架/驱动)→ 接口适配器 → 用例层 → 实体层(核心)依赖规则:依赖方向只能从外向内,内层不知道外层的存在。
| 层次 | 职责 | 示例 |
|---|---|---|
| 实体层 | 核心业务规则,与框架无关 | Order、User 领域对象 |
| 用例层 | 应用业务规则,编排领域对象 | CreateOrderUseCase |
| 接口适配器 | 转换数据格式 | Controller、Repository |
| 框架层 | 技术细节 | Spring、MyBatis、Redis |
6. 架构反模式
| 反模式 | 描述 | 后果 | 正确做法 |
|---|---|---|---|
| 分布式单体 | 拆成了微服务,但服务间强耦合,必须一起部署 | 兼具单体和微服务的缺点 | 按业务域拆分,保持高内聚低耦合 |
| 过早优化 | 业务还没验证就搭建复杂架构 | 浪费时间,增加维护成本 | 先用最简单的方案,遇到瓶颈再优化 |
| 简历驱动开发 | 为了用新技术而引入不必要的复杂度 | 团队学习成本高,稳定性差 | 技术选型以解决问题为导向 |
| 大泥球 | 没有清晰的模块边界,代码随意调用 | 牵一发动全身,无法维护 | 明确模块边界,通过接口通信 |
| 拆分过细 | 每个接口都是一个服务 | 网络开销大,分布式事务多 | 按业务域识别边界,域与服务不是一一对应 |
7. 常见问题
Q:微服务和单体架构如何选择?
初创项目优先模块化单体,快速验证业务。当出现独立发布、独立扩缩容、故障隔离等明确需求,且团队具备相应治理能力时,再考虑微服务拆分(团队规模只是参考信号之一)。不要为了微服务而微服务,微服务的运维复杂度远高于单体。
Q:服务拆分的常见错误是什么?
① 拆分过细,每个接口都跨服务调用,导致网络开销和分布式事务问题;② 拆分后服务间仍然强耦合,必须一起部署(分布式单体);③ 没有建设好基础设施(注册发现、链路追踪、CI/CD)就急于拆分。应按业务域拆分,保持高内聚低耦合。
Q:SOA 和微服务的区别是什么?
二者在服务粒度、治理方式、自治程度和组织协作方式上存在常见差异:SOA 通常服务粒度较粗、依赖集中式总线(ESB)统一治理;微服务服务粒度更细、每个服务独立部署、团队自治程度更高,服务间多用轻量级协议(HTTP/gRPC)或消息中间件通信。但差异是程度与治理取向上的,不是有无 ESB 的绝对边界,微服务可以看作 SOA 的轻量化演进。
Q:什么是六边形架构?它和传统三层架构有什么区别?
六边形架构将业务逻辑放在中心,通过端口(接口)与外部交互,适配器负责技术细节。与三层架构的区别在于:三层架构的依赖方向是从上到下(Controller → Service → DAO),业务层依赖数据层;六边形架构通过依赖倒置,核心域不依赖任何外部技术,更容易测试和替换技术组件。
Q:如何判断是否需要从单体迁移到微服务?
出现以下信号时考虑迁移:① 团队规模增长导致代码合并冲突频繁(人数只是参考,关键是沟通与协调成本);② 某个模块需要独立扩展(如秒杀模块需要 10 倍资源,其他模块不需要);③ 发布频率受限,一个模块的变更需要等待整体发布;④ 技术栈需要多样化(如 AI 模块用 Python,其他用 Java)。
8. Conway 定律与架构约束
技术约束(用户规模、成本、性能)大家都想得到,但有一个约束比它们都大,却经常被忽略——人。
"设计系统的组织,其产生的设计等同于组织之间的沟通结构。" —— Melvin Conway, 1967
用人话说:系统架构往往会反映团队的沟通结构。
3 人团队硬拆 6 个微服务:
服务 A(无人维护)→ 版本落后 → 成为技术债
服务 B(张三维护)→ 张三离职 → 无人能改
其余服务 → 同样的问题
3 个团队各 5 人,坚持单体架构:
团队 A 改了订单模块 → 冲突了团队 B 的支付模块
团队 B 等团队 A 发布 → 交付周期拉长
合并冲突、代码审查、发布协调 → 效率暴跌| 约束类型 | 示例 | 影响 |
|---|---|---|
| 团队规模 | 3 人团队不适合微服务 | 运维成本 > 开发收益 |
| 团队分布 | 跨时区团队需要服务解耦 | 同步协作成本太高 |
| 技术栈 | 团队只熟悉 Java,不熟悉 Go | 强行换栈的学习成本 |
| 组织边界 | 两个部门各管一个服务 | API 契约成为部门边界 |
经验法则:先看团队结构,再定架构。团队规模是重要的组织约束,但不是拆分的充分条件。团队只有 5 个人时,模块化单体通常是最佳选择;等团队增长、模块间沟通成本超过拆分成本,并出现独立发布、扩缩容或故障隔离的真实需求时,再考虑微服务。
9. 模块边界设计
无论选哪种架构风格,最终落地都要靠模块边界保障。好的模块边界是架构的"防波堤"——变化发生时,边界阻止变化扩散。
9.1 高内聚、低耦合
高内聚(模块内部紧密相关) 低耦合(模块之间松散依赖)
┌─────────────────┐ ┌────────┐ ┌────────┐
│ Order 模块 │ │ Order │ │ User │
│ - Order │ │ Module │◄──►│ Module │
│ - OrderItem │ │ │ │ │
│ - OrderService │ └────────┘ └────────┘
│ - OrderRepo │ 仅通过接口通信
└─────────────────┘
内部元素高度相关9.2 单一职责在模块层面
// ❌ 反模式:一个 Service 做所有事
@Service
public class GodService {
public void createOrder() { ... } // 订单
public void registerUser() { ... } // 用户
public void processPayment() { ... }// 支付
public void sendEmail() { ... } // 通知
}
// ✅ 正确:按领域拆分模块
public class OrderService { /* 只管订单 */ }
public class UserService { /* 只管用户 */ }
public class PaymentService { /* 只管支付 */ }
public class NotificationService { /* 只管通知 */ }9.3 模块间通信的两种模式
// 模式一:直接方法调用(单体内模块间)
public class OrderService {
private final PaymentService paymentService;
public void createOrder(OrderRequest request) {
Order order = Order.create(request);
paymentService.charge(order.getId(), order.getTotalAmount());
}
}
// 模式二:事件驱动(单体内模块间松耦合)
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public void createOrder(OrderRequest request) {
Order order = Order.create(request);
orderRepository.save(order);
eventPublisher.publishEvent(new OrderCreatedEvent(order.getId(), order.getTotalAmount()));
// 不直接调用 PaymentService,由事件监听器处理
}
}一致性边界:
ApplicationEventPublisher发布的是应用内事件,默认由当前进程内的监听器处理,适用于单体内模块间通知,并不自动把事件发送到其他服务;跨进程或跨服务的事件通信需要消息代理等明确传输机制。同时它默认不保证数据库提交与事件处理具备原子性。若事件必须在事务提交后处理,可用@TransactionalEventListener(AFTER_COMMIT),但它不能解决进程在提交后、事件处理前崩溃导致的事件丢失。需要可靠地把业务变更传播到消息系统时,应使用事务 Outbox 模式,并配合投递重试与消费幂等(可参考消息集成与分布式事务相关文档)。
9.4 边界守护:ArchUnit
模块边界不是画出来的,而是守出来的。没有自动化测试守护的边界,迟早被破坏:
@Test
void serviceShouldNotDependOnController() {
noClasses()
.that().resideInAPackage("..service..")
.should().dependOnClassesThat()
.resideInAPackage("..controller..")
.check(importedClasses);
}
@Test
void domainShouldNotDependOnInfrastructure() {
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat()
.resideInAPackage("..infrastructure..")
.check(importedClasses);
}