Skip to content

软件架构演进 ​

核心问题:软件架构经历了哪些演进阶段?每种架构解决了什么问题、引入了什么新问题?如何根据业务阶段选择合适的架构?

1. 它解决了什么问题? ​

架构不是一成不变的,而是随着业务规模、团队规模、技术能力的变化而演进的。理解架构演进的脉络,能帮助你:

  • 在正确的时间点做出正确的架构决策(不过度设计,也不欠设计)
  • 理解每种架构的适用场景和局限性
  • 避免"为了微服务而微服务"的常见陷阱

2. 架构演进全景 ​

3. 各阶段架构详解 ​

3.1 单体架构(Monolith) ​

所有功能模块打包在一个应用中,共享一个数据库,一次部署。

txt
┌─────────────────────────────────┐
│           单体应用               │
│  ┌─────┐ ┌─────┐ ┌─────┐      │
│  │用户  │ │订单  │ │商品  │      │
│  │模块  │ │模块  │ │模块  │      │
│  └──┬──┘ └──┬──┘ └──┬──┘      │
│     └───────┼───────┘          │
│          ┌──┴──┐               │
│          │ DB  │               │
│          └─────┘               │
└─────────────────────────────────┘
优势劣势
开发简单,IDE 直接调试统一部署,单个部署单元发布影响范围大
部署简单,一个 JAR/WAR难以按模块独立扩缩容
事务简单,本地事务即可代码量增大后编译与启动可能变慢
适合快速验证业务同一部署单元需共享兼容运行环境,混合技术栈成本高

注意:代码耦合、启动缓慢属于实现质量与代码规模问题,并非单体架构的必然属性。设计良好的模块化单体同样能做到高内聚、低耦合,表中所列为常见风险而非必然缺陷。

适用场景:初创项目、MVP 验证、团队 1-5 人、业务逻辑简单。

3.2 垂直拆分架构 ​

按业务线将单体拆分为多个独立应用,各应用通常拥有自己的数据库。

txt
┌──────────┐  ┌──────────┐  ┌──────────┐
│ 用户系统  │  │ 订单系统   │  │ 商品系统  │
│          │  │          │  │          │
│  ┌────┐  │  │  ┌────┐  │  │  ┌────┐  │
│  │ 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 传统三层架构 ​

txt
Controller(表现层)→ Service(业务层)→ DAO(数据层)

问题:一种常见的退化方式是业务逻辑全部堆在 Service 层,Entity 只有 getter/setter(贫血模型),Service 越来越臃肿。这是常见实现问题,并非三层架构的必然结果。

5.2 六边形架构(端口与适配器) ​

核心思想:业务逻辑在中心,通过端口(接口)与外部交互,适配器负责技术细节。更换数据库、消息队列等只需替换适配器,不影响核心业务。

5.3 整洁架构(Clean Architecture) ​

txt
外层(框架/驱动)→ 接口适配器 → 用例层 → 实体层(核心)

依赖规则:依赖方向只能从外向内,内层不知道外层的存在。

层次职责示例
实体层核心业务规则,与框架无关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

用人话说:系统架构往往会反映团队的沟通结构。

txt
3 人团队硬拆 6 个微服务:
  服务 A(无人维护)→ 版本落后 → 成为技术债
  服务 B(张三维护)→ 张三离职 → 无人能改
  其余服务 → 同样的问题

3 个团队各 5 人,坚持单体架构:
  团队 A 改了订单模块 → 冲突了团队 B 的支付模块
  团队 B 等团队 A 发布 → 交付周期拉长
  合并冲突、代码审查、发布协调 → 效率暴跌
约束类型示例影响
团队规模3 人团队不适合微服务运维成本 > 开发收益
团队分布跨时区团队需要服务解耦同步协作成本太高
技术栈团队只熟悉 Java,不熟悉 Go强行换栈的学习成本
组织边界两个部门各管一个服务API 契约成为部门边界

经验法则:先看团队结构,再定架构。团队规模是重要的组织约束,但不是拆分的充分条件。团队只有 5 个人时,模块化单体通常是最佳选择;等团队增长、模块间沟通成本超过拆分成本,并出现独立发布、扩缩容或故障隔离的真实需求时,再考虑微服务。

9. 模块边界设计 ​

无论选哪种架构风格,最终落地都要靠模块边界保障。好的模块边界是架构的"防波堤"——变化发生时,边界阻止变化扩散。

9.1 高内聚、低耦合 ​

txt
    高内聚(模块内部紧密相关)         低耦合(模块之间松散依赖)

    ┌─────────────────┐           ┌────────┐    ┌────────┐
    │  Order 模块      │           │ Order  │    │ User   │
    │  - Order         │           │ Module │◄──►│ Module │
    │  - OrderItem     │           │        │    │        │
    │  - OrderService  │           └────────┘    └────────┘
    │  - OrderRepo     │               仅通过接口通信
    └─────────────────┘
      内部元素高度相关

9.2 单一职责在模块层面 ​

java
// ❌ 反模式:一个 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 模块间通信的两种模式 ​

java
// 模式一:直接方法调用(单体内模块间)
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 ​

模块边界不是画出来的,而是守出来的。没有自动化测试守护的边界,迟早被破坏:

java
@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);
}

Released under the MIT License.