开篇黄金100字:
你是不是也经历过这样的场景?老板一拍桌子说"系统太卡了,拆微服务!",然后团队吭哧吭哧拆了半年,发现复杂度爆炸、运维成本飙升,最后性能反而更差了。其实,单体架构从来不是原罪,混乱的代码组织才是。今天,我想和你聊聊如何用模块化单体(Modular Monolith)在不拆微服务的情况下,让老系统焕发新生。
📋 目录
- 1. 单体架构:被误解的"背锅侠"
- 2. 模块化单体:单体与微服务之间的第三条路
- 3. Spring Modulith实战:从混乱到有序
- 4. 实战案例:医疗系统的模块化改造
- 5. 总结:什么时候选模块化单体?
- 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. 模块边界定义:通过包结构定义模块
- 2. 依赖验证:编译期检查模块依赖,防止违规调用
- 3. 事件驱动:模块间通过事件解耦
- 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) | 150 | 450 | 200% |
| 发布耗时 | 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. 你的项目中,哪些业务边界是模糊的?如何划分更合理?
- 2. 如果让你改造现有单体系统,你会选择模块化单体还是微服务?为什么?
- 3. 模块化单体的最大挑战是什么?(提示:不是技术问题)
📚 系列预告
- • 下一篇:《Spring Modulith进阶:事件驱动与CQRS实践》
- • 再下一篇:《从模块化单体平滑迁移到微服务:一条可行的路》
💬 互动时间
投票:你的系统是单体还是微服务?
- • A. 混乱单体(大泥球)
- • B. 模块化单体
- • C. 微服务架构
- • D. 正在从单体拆微服务中...
欢迎在评论区留下你的选择和踩过的坑!
关于作者:10年Java后端开发,经历过单体→SOA→微服务→模块化单体的完整周期。相信架构没有银弹,只有适合当前阶段的方案。
标签:单体架构 模块化 Spring Modulith 架构改造 微服务 后端开发 Java

1596

被折叠的 条评论
为什么被折叠?



