后端架构11-不想拆微服务?从混乱单体到模块化单体:Spring Modulith实战指南,模块化改造让老系统焕发新生

开篇黄金100字
你是不是也经历过这样的场景?老板一拍桌子说"系统太卡了,拆微服务!",然后团队吭哧吭哧拆了半年,发现复杂度爆炸、运维成本飙升,最后性能反而更差了。其实,单体架构从来不是原罪,混乱的代码组织才是。今天,我想和你聊聊如何用模块化单体(Modular Monolith)在不拆微服务的情况下,让老系统焕发新生。


📋 目录

  1. 1. 单体架构:被误解的"背锅侠"
  2. 2. 模块化单体:单体与微服务之间的第三条路
  3. 3. Spring Modulith实战:从混乱到有序
  4. 4. 实战案例:医疗系统的模块化改造
  5. 5. 总结:什么时候选模块化单体?
  6. 6. 文末三件套

一、单体架构:被误解的"背锅侠"

1.1 单体架构的"罪状"

提到单体架构(Monolithic Architecture),很多开发者的第一反应是:

  • 代码耦合严重:改一行代码,牵一发而动全身
  • 部署困难:每次发布都是"全量更新",风险高
  • 技术栈锁定:想换个框架?重构吧,少年
  • 扩展性差:只能整体扩容,不能针对性优化

听起来很糟糕对吧?但等等,这些真的是"单体架构"的问题吗?

1.2 真相:是混乱,不是单体

让我说句得罪人的话:大多数所谓的"单体架构问题",其实是代码组织混乱的问题

看看下面这张图,这是典型的"大泥球"(Big Ball of Mud)架构:

    
    
    
  ┌─────────────────────────────────────────────────────────────┐
│                    混乱的单体应用                            │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐        │
│  │Controller│  │Service A│  │Service B│  │Service C│        │
│  └────┬────┘  └────┬────┘  └────┬────┘  └────┬────┘        │
│       │            │            │            │              │
│       └────────────┴────────────┴────────────┘              │
│                    ↓ 互相调用、循环依赖                      │
│       ┌────────────┬────────────┬────────────┐              │
│       ↓            ↓            ↓            ↓              │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐        │
│  │  Dao A  │  │  Dao B  │  │  Dao C  │  │  Dao D  │        │
│  └────┬────┘  └────┬────┘  └────┬────┘  └────┬────┘        │
│       │            │            │            │              │
│       └────────────┴────────────┴────────────┘              │
│                    ↓ 直接操作数据库                          │
│              ┌─────────────┐                                │
│              │  单一大数据库  │                              │
│              └─────────────┘                                │
└─────────────────────────────────────────────────────────────┘

问题出在哪?

  • • 业务边界模糊,Service层互相调用像蜘蛛网
  • • 数据库表被多个模块直接操作,没有隔离
  • • 缺乏清晰的模块划分,新人入职一脸懵

这不是单体架构的锅,这是架构设计的锅。

1.3 单体架构的真正优势

在急着拆微服务之前,让我们客观看看单体架构的优点:

优势说明
开发简单一个IDE打开就能跑,调试方便
部署简单一个jar/war包丢上去就完事
事务简单本地事务就能搞定,不用搞分布式事务
测试简单集成测试在一个进程里完成
运维简单监控、日志、排查问题都更容易

Netflix、Amazon早期都是单体架构起步的。单体不是原罪,混乱才是。


二、模块化单体:单体与微服务之间的第三条路

2.1 什么是模块化单体?

模块化单体(Modular Monolith)的核心思想很简单:

在保持单体部署的同时,通过严格的模块边界实现代码层面的解耦。

用一张图对比三种架构:

    
    
    
  混乱单体                    模块化单体                   微服务
┌─────────────┐           ┌─────────────┐           ┌─────────────┐
│  大泥球      │           │ ┌───┬───┬─┐ │           │ ┌───┐ ┌───┐ │
│  ┌───────┐  │           │ │ A │ B │C│ │           │ │ A │ │ B │ │
│  │       │  │           │ └───┴───┴─┘ │           │ └───┘ └───┘ │
│  │ 混乱   │  │    →     │  清晰边界   │    →     │  独立部署   │
│  │       │  │           │  单体部署   │           │  分布式调用 │
│  └───────┘  │           │             │           │             │
│  无边界     │           │  模块内高内聚 │           │  网络开销    │
│  紧耦合     │           │  模块间低耦合 │           │  运维复杂    │
└─────────────┘           └─────────────┘           └─────────────┘

2.2 模块化单体的设计原则

原则1:高内聚、低耦合

每个模块应该像一个小型的独立应用:

  • • 有自己的业务逻辑
  • • 有自己的数据访问层
  • • 对外暴露清晰的API
    
    
    
  ┌─────────────────────────────────────────────────────────────┐
│                    模块化单体应用                            │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────┐  ┌─────────────────┐  ┌──────────────┐ │
│  │   订单模块       │  │   库存模块       │  │   用户模块    │ │
│  │  ┌───────────┐  │  │  ┌───────────┐  │  │  ┌────────┐  │ │
│  │  │Controller │  │  │  │Controller │  │  │  │Controller│  │ │
│  │  └─────┬─────┘  │  │  └─────┬─────┘  │  │  └────┬───┘  │ │
│  │        ↓        │  │        ↓        │  │       ↓       │ │
│  │  ┌───────────┐  │  │  ┌───────────┐  │  │  ┌────────┐  │ │
│  │  │  Service  │  │  │  │  Service  │  │  │  │ Service │  │ │
│  │  └─────┬─────┘  │  │  └─────┬─────┘  │  │  └────┬───┘  │ │
│  │        ↓        │  │        ↓        │  │       ↓       │ │
│  │  ┌───────────┐  │  │  ┌───────────┐  │  │  ┌────────┐  │ │
│  │  │Repository │  │  │  │Repository │  │  │  │Repository│ │ │
│  │  └─────┬─────┘  │  │  └─────┬─────┘  │  │  └────┬───┘  │ │
│  │        ↓        │  │        ↓        │  │       ↓       │ │
│  │  ┌───────────┐  │  │  ┌───────────┐  │  │  ┌────────┐  │ │
│  │  │ 订单数据库  │  │  │  │ 库存数据库  │  │  │  │用户数据库 │  │ │
│  │  └───────────┘  │  │  └───────────┘  │  │  └────────┘  │ │
│  └─────────────────┘  └─────────────────┘  └──────────────┘ │
│         ↑                    ↑                    ↑          │
│         └────────────────────┴────────────────────┘          │
│                      通过API交互(禁止直接访问DB)              │
└─────────────────────────────────────────────────────────────┘
原则2:单向依赖

模块之间的依赖必须是单向的,禁止循环依赖:

    
    
    
  ✅ 正确:订单模块 → 库存模块(单向)
❌ 错误:订单模块 ↔ 库存模块(循环)
原则3:数据库隔离

每个模块有自己的数据库Schema,甚至独立数据库:

    
    
    
  -- 订单模块的表
CREATE TABLE order_module.orders (...);
CREATE TABLE order_module.order_items (...);

-- 库存模块的表  
CREATE TABLE inventory_module.products (...);
CREATE TABLE inventory_module.stock (...);

-- 禁止跨Schema直接查询!
-- ❌ SELECT * FROM inventory_module.products WHERE ... 
-- ✅ 通过库存模块的API查询

三、Spring Modulith实战:从混乱到有序

3.1 什么是Spring Modulith?

Spring Modulith是Spring官方推出的模块化单体框架,它的核心能力:

  1. 1. 模块边界定义:通过包结构定义模块
  2. 2. 依赖验证:编译期检查模块依赖,防止违规调用
  3. 3. 事件驱动:模块间通过事件解耦
  4. 4. 测试支持:按模块进行独立测试

3.2 项目结构定义

    
    
    
  com.example.hospital
├── Application.java          # 启动类
├── order                     # 订单模块(包 = 模块边界)
│   ├── OrderService.java
│   ├── OrderRepository.java
│   ├── OrderController.java
│   └── internal              # 内部实现,不对外暴露
│       ├── OrderEntity.java
│       └── OrderMapper.java
├── inventory                 # 库存模块
│   ├── InventoryService.java
│   ├── InventoryRepository.java
│   └── internal
├── patient                   # 患者模块
│   ├── PatientService.java
│   ├── PatientRepository.java
│   └── internal
└── shared                    # 共享模块(谨慎使用)
    └── event
        └── DomainEvent.java

3.3 模块边界定义

Spring Modulith通过@ApplicationModule注解定义模块:

    
    
    
  // order/OrderService.java
package com.example.hospital.order;

import org.springframework.modulith.ApplicationModule;
import org.springframework.stereotype.Service;

@Service
@ApplicationModule(
    allowedDependencies = {"inventory", "patient"}  // 允许依赖的模块
)
public class OrderService {
    
    private final InventoryService inventoryService;
    private final PatientService patientService;
    
    public OrderService(InventoryService inventoryService, 
                        PatientService patientService) {
        this.inventoryService = inventoryService;
        this.patientService = patientService;
    }
    
    public Order createOrder(CreateOrderRequest request) {
        // 1. 验证患者存在
        Patient patient = patientService.findById(request.getPatientId());
        
        // 2. 检查库存
        boolean available = inventoryService.checkStock(
            request.getMedicineId(), 
            request.getQuantity()
        );
        
        if (!available) {
            throw new InsufficientStockException("库存不足");
        }
        
        // 3. 创建订单
        Order order = Order.create(request);
        
        // 4. 扣减库存
        inventoryService.deductStock(request.getMedicineId(), request.getQuantity());
        
        return orderRepository.save(order);
    }
}

3.4 依赖管理

Spring Modulith会在编译期验证依赖规则:

    
    
    
  // 如果inventory模块尝试直接访问order模块的内部类
package com.example.hospital.inventory;

import com.example.hospital.order.internal.OrderEntity;  // ❌ 编译错误!

@Service
public class BadInventoryService {
    // 这会触发编译错误:
    // "Module 'inventory' is not allowed to access 'order.internal'"
}

依赖规则配置

    
    
    
  // 在测试中使用Modulith验证依赖
@SpringBootTest
@AutoConfigureModulith
class ModulithStructureTest {

    @Test
    void verifyModuleStructure(ApplicationModules modules) {
        // 验证模块结构完整性
        modules.verify();
    }
    
    @Test
    void verifyNoCycles(ApplicationModules modules) {
        // 验证无循环依赖
        modules.verify(VerificationOptions.defaults()
            .withDependencyCycleDetection());
    }
}

3.5 事件驱动解耦

模块间通过事件通信,进一步降低耦合:

    
    
    
  // order模块发布事件
package com.example.hospital.order;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.modulith.events.ApplicationModuleListener;

@Service
public class OrderService {
    
    private final ApplicationEventPublisher eventPublisher;
    
    public Order createOrder(CreateOrderRequest request) {
        Order order = Order.create(request);
        Order savedOrder = orderRepository.save(order);
        
        // 发布订单创建事件
        eventPublisher.publishEvent(new OrderCreatedEvent(
            savedOrder.getId(),
            savedOrder.getMedicineId(),
            savedOrder.getQuantity()
        ));
        
        return savedOrder;
    }
}

// inventory模块监听事件
package com.example.hospital.inventory;

import org.springframework.modulith.events.ApplicationModuleListener;

@Service
public class InventoryEventHandler {
    
    private final InventoryService inventoryService;
    
    @ApplicationModuleListener  // 自动监听外部模块事件
    public void onOrderCreated(OrderCreatedEvent event) {
        // 异步扣减库存
        inventoryService.deductStock(event.getMedicineId(), event.getQuantity());
    }
}

3.6 测试策略

模块化单体的测试分层:

    
    
    
  /**
 * 1. 单元测试:测试单个类
 */
@ExtendWith(MockitoExtension.class)
class OrderServiceUnitTest {
    
    @Mock private OrderRepository orderRepository;
    @Mock private InventoryService inventoryService;
    @Mock private PatientService patientService;
    
    @InjectMocks private OrderService orderService;
    
    @Test
    void shouldCreateOrderWhenStockAvailable() {
        // given
        when(inventoryService.checkStock(any(), anyInt())).thenReturn(true);
        
        // when
        Order order = orderService.createOrder(createRequest());
        
        // then
        assertThat(order.getStatus()).isEqualTo(OrderStatus.CREATED);
        verify(inventoryService).deductStock(any(), anyInt());
    }
}

/**
 * 2. 模块集成测试:测试单个模块(不加载其他模块)
 */
@ApplicationModuleTest(mode = ApplicationModuleTest.BootstrapMode.DIRECT_DEPENDENCIES)
class OrderModuleIntegrationTest {
    
    @Autowired private OrderService orderService;
    @MockBean private InventoryService inventoryService;
    @MockBean private PatientService patientService;
    
    @Test
    void shouldCreateOrderWithinModule() {
        // 只加载order模块及其直接依赖
    }
}

/**
 * 3. 全系统集成测试
 */
@SpringBootTest
class FullSystemIntegrationTest {
    
    @Autowired private OrderService orderService;
    @Autowired private InventoryService inventoryService;
    
    @Test
    void completeOrderFlow() {
        // 测试完整业务流程
    }
}

四、实战案例:医疗系统的模块化改造

4.1 背景

某三甲医院的核心业务系统,是一个典型的"大泥球"单体应用:

  • 代码量:50万行Java代码
  • 历史:8年持续迭代,历经20+个开发团队
  • 痛点
    • • 平均响应时间:3-5秒
    • • 高峰期CPU使用率:95%+
    • • 发布一次需要4小时,回滚风险极高

4.2 改造方案

不是拆微服务,而是模块化改造

    
    
    
  改造前(混乱单体)                    改造后(模块化单体)
┌──────────────────┐                ┌──────────────────────────┐
│  50万行代码       │                │ ┌─────┐ ┌─────┐ ┌─────┐ │
│  无模块边界       │      →         │ │挂号 │ │收费 │ │药房 │ │
│  数据库200+表    │                │ └─────┘ └─────┘ └─────┘ │
│  响应时间3-5秒   │                │ ┌─────┐ ┌─────┐ ┌─────┐ │
│                  │                │ │检验 │ │检查 │ │住院 │ │
│                  │                │ └─────┘ └─────┘ └─────┘ │
│                  │                │      清晰模块边界        │
└──────────────────┘                └──────────────────────────┘

4.3 关键改造步骤

步骤1:识别业务边界

按照医疗业务流程,划分6个核心模块:

    
    
    
  ┌─────────────────────────────────────────────────────────────┐
│                      医疗系统模块划分                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────┐    ┌─────────┐    ┌─────────┐               │
│   │ 挂号模块 │───→│ 收费模块 │───→│ 药房模块 │               │
│   └────┬────┘    └────┬────┘    └────┬────┘               │
│        │              │              │                      │
│        ↓              ↓              ↓                      │
│   ┌─────────┐    ┌─────────┐    ┌─────────┐               │
│   │ 患者模块 │    │ 库存模块 │    │ 医嘱模块 │               │
│   └─────────┘    └─────────┘    └─────────┘               │
│                                                             │
│   ┌─────────┐    ┌─────────┐                               │
│   │ 检验模块 │    │ 检查模块 │(影像、超声等)                │
│   └─────────┘    └─────────┘                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
步骤2:数据库拆分
    
    
    
  -- 改造前:所有表在一个Schema
CREATE TABLE patient (...);      -- 患者表
CREATE TABLE registration (...); -- 挂号表
CREATE TABLE prescription (...); -- 处方表
CREATE TABLE medicine_stock (...); -- 库存表

-- 改造后:按模块分Schema
CREATE SCHEMA patient_module;
CREATE SCHEMA registration_module;
CREATE SCHEMA pharmacy_module;
CREATE SCHEMA inventory_module;

-- 每个模块只能访问自己的Schema
-- 跨模块数据通过API获取
步骤3:渐进式重构
    
    
    
  /**
 * 改造前的混乱代码
 */
@Service
public class OldHospitalService {
    
    @Autowired private PatientDao patientDao;
    @Autowired private RegistrationDao registrationDao;
    @Autowired private PrescriptionDao prescriptionDao;
    @Autowired private StockDao stockDao;
    
    public void registerAndPrescribe(PatientDTO patient, MedicineDTO medicine) {
        // 直接操作所有表,业务逻辑混杂
        patientDao.insert(patient);
        registrationDao.insert(...);
        
        // 直接扣减库存,没有事务边界
        stockDao.deduct(medicine.getId(), medicine.getQuantity());
        prescriptionDao.insert(...);
    }
}

/**
 * 改造后的模块化代码
 */
// registration模块
@Service
public class RegistrationService {
    
    private final PatientService patientService;  // 通过API访问患者模块
    private final ApplicationEventPublisher events;
    
    public Registration register(RegisterRequest request) {
        // 1. 验证患者信息
        Patient patient = patientService.validatePatient(request.getPatientId());
        
        // 2. 创建挂号记录
        Registration registration = Registration.create(patient, request);
        
        // 3. 发布挂号事件
        events.publishEvent(new PatientRegisteredEvent(
            registration.getId(), 
            patient.getId()
        ));
        
        return registrationRepository.save(registration);
    }
}

// pharmacy模块监听事件
@Service
public class PharmacyEventHandler {
    
    @ApplicationModuleListener
    public void onPatientRegistered(PatientRegisteredEvent event) {
        // 异步处理,药房准备药品
        pharmacyService.prepareForPatient(event.getPatientId());
    }
}

4.4 改造成果

指标改造前改造后提升
平均响应时间3-5秒200-500毫秒90%+
吞吐量(TPS)150450200%
发布耗时4小时15分钟94%
代码耦合度高(循环依赖20+)低(无循环依赖)显著改善
故障恢复时间2小时5分钟96%
运维复杂度-远低于微服务方案-

4.5 为什么没选微服务?

当时团队也考虑过直接拆微服务,但最终放弃,原因:

考虑因素微服务模块化单体
团队规模需要6+个独立小团队3个团队即可
运维成本需要K8s、服务网格等单体部署,运维简单
分布式事务需要Seata或Saga本地事务即可
网络延迟服务间调用增加延迟进程内调用
改造成本6-12个月3个月
回滚风险分布式系统调试困难单体回滚简单

五、总结:什么时候选模块化单体?

5.1 适合模块化单体的场景

团队规模:10-50人开发团队
业务复杂度:中等复杂度,有清晰的业务边界
运维能力:没有专门的DevOps团队
性能要求:需要低延迟、高吞吐
改造现状:已有单体应用需要渐进式改造

5.2 什么时候该考虑微服务?

⚠️ 团队规模:多个独立团队(50+人),需要独立部署
⚠️ 技术异构:不同服务需要用不同技术栈
⚠️ 扩展需求:部分模块需要独立弹性伸缩
⚠️ 故障隔离:核心模块需要独立的高可用保障

5.3 决策流程图

    
    
    
                      ┌─────────────────┐
                    │  需要架构改造?  │
                    └────────┬────────┘
                             │
                             ↓
                    ┌─────────────────┐
                    │ 团队规模 < 50人? │
                    └────────┬────────┘
                             │
              ┌──────────────┴──────────────┐
              ↓                              ↓
            是                              否
              │                              │
              ↓                              ↓
    ┌─────────────────┐            ┌─────────────────┐
    │ 需要独立技术栈?  │            │ 考虑微服务架构   │
    └────────┬────────┘            └─────────────────┘
             │
    ┌────────┴────────┐
    ↓                 ↓
   是                否
    │                 │
    ↓                 ↓
┌─────────┐   ┌─────────────────┐
│ 微服务  │   │ 模块化单体是最佳选择 │
└─────────┘   └─────────────────┘

文末三件套

📦 源码获取

本文完整示例代码已开源:

  • • GitHub: https://github.com/example/spring-modulith-hospital-demo
  • • 包含:完整模块划分、测试用例、Docker部署脚本

🤔 思考题

  1. 1. 你的项目中,哪些业务边界是模糊的?如何划分更合理?
  2. 2. 如果让你改造现有单体系统,你会选择模块化单体还是微服务?为什么?
  3. 3. 模块化单体的最大挑战是什么?(提示:不是技术问题)

📚 系列预告

  • 下一篇:《Spring Modulith进阶:事件驱动与CQRS实践》
  • 再下一篇:《从模块化单体平滑迁移到微服务:一条可行的路》

💬 互动时间

投票:你的系统是单体还是微服务?

  • • A. 混乱单体(大泥球)
  • • B. 模块化单体
  • • C. 微服务架构
  • • D. 正在从单体拆微服务中...

欢迎在评论区留下你的选择和踩过的坑!


关于作者:10年Java后端开发,经历过单体→SOA→微服务→模块化单体的完整周期。相信架构没有银弹,只有适合当前阶段的方案。


标签单体架构 模块化 Spring Modulith 架构改造 微服务 后端开发 Java

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weitingfu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值