单体还是微服务:如何正确选择

1. 概述

关于“该选择单体还是微服务”的问题在几乎每场系统设计讨论中都会出现。本文不提供万能答案,而是通过具体的标准来分析:团队规模、发布速度、运维复杂度以及服务间的通信成本。读完本文后,你将拥有一份检查清单,能够自己评估系统应该处于“单体——模块化单体——微服务”这个轴的哪个位置。

2. 单体:简单但容易被低估

单体意味着所有业务(订单、客户、支付、库存……)在一个进程、一个代码库、一次部署中运行。

@RestController
@RequestMapping("/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
        Order order = orderService.create(request);
        return ResponseEntity.ok(OrderResponse.from(order));
    }
}

@Service
public class OrderService {

    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    public OrderService(InventoryService inventoryService, PaymentService paymentService) {
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
    }

    public Order create(OrderRequest request) {
        inventoryService.reserve(request.productId(), request.quantity());
        paymentService.charge(request.customerId(), request.amount());
        return new Order(request); // 假设构造函数负责保存到数据库
    }
}

OrderService 直接通过同一 JVM 内的方法调用 InventoryService 和 PaymentService。没有网络,没有序列化,事务可以包装在单个 @Transactional 中。这是单体最大的优势:容易保证一致性、容易调试、一次部署就完成。

缺点出现在团队扩大时:每个人都在修改同一个代码库,构建时间增加,部署一个小模块的风险也会导致整个应用重新构建。

注意: 单体不等同于“代码混乱”。一个写得好的单体,具有清晰的模块(按领域分包,没有交叉依赖),仍然可以很好地为百万级用户系统运行。

3. 微服务:分离以独立扩展,但代价是运维复杂性

微服务将订单、库存、支付分离为独立的服务,通过网络(REST、gRPC、消息队列)进行通信。

@Service
public class OrderService {

    private final RestClient restClient;

    public OrderService(RestClient restClient) {
        this.restClient = restClient;
    }

    public Order create(OrderRequest request) {
        // 通过HTTP调用Inventory Service
        restClient.post()
                .uri("http://inventory-service/reservations")
                .body(new ReservationRequest(request.productId(), request.quantity()))
                .retrieve()
                .toBodilessEntity();

        // 通过HTTP调用Payment Service
        restClient.post()
                .uri("http://payment-service/charges")
                .body(new ChargeRequest(request.customerId(), request.amount()))
                .retrieve()
                .toBodilessEntity();

        return new Order(request);
    }
}

与第 2 章的单体版本相比,区别在于 restClient.post(...):进程内方法调用变成了网络上的 HTTP 调用。后果:

  • 每次调用都可能因网络超时、服务宕机、DNS 错误而失败——这些错误在单体中不存在。
  • 不再有跨服务的 ACID 事务。您必须使用 saga 模式或最终一致性来处理。
  • 需要额外的服务发现、负载均衡、分布式追踪来在出错时进行调试。

3.1. 隐藏成本:分布式事务

@Service
public class OrderSagaOrchestrator {

    public void createOrder(OrderRequest request) {
        try {
            inventoryClient.reserve(request.productId(), request.quantity());
            paymentClient.charge(request.customerId(), request.amount());
        } catch (PaymentFailedException e) {
            // 必须自己编写补偿逻辑来回滚
            inventoryClient.release(request.productId(), request.quantity());
            throw e;
        }
    }
}

在单体中,回滚由 @Transactional 自动处理。在微服务中,您必须自己编写补偿逻辑(reserve 失败后 release)。这是单体版本中不存在但在微服务中必须存在的代码,以避免数据不一致。

4. 决定标准:不仅仅是“大公司在做什么”

4.1. 团队规模和康威定律

康威定律:系统结构反映了组织结构。如果你有 5 个开发人员,分成 5 个服务意味着每个人负责一个服务——没有人互相审查代码,你失去了“多人看一个 bug”的优势。

实际参考阈值:

1–15 个开发者,1 个团队      -> 单体(模块化)
15–50 个开发者,2–5 个团队   -> 模块化单体或分离2-3个大服务
50+ 个开发者,多个团队        -> 按bounded context的微服务

4.2. 发布速度真的是瓶颈吗?

如果你的团队每两周发布一次单体且没有严重的合并冲突,分离服务来“独立发布”不会带来价值——它只会增加运维成本(独立的 CI/CD、独立的监控、API 版本管理)。

微服务值得投资的时机是:团队 A 需要为支付功能每天部署 10 次,而团队 B 的库存功能只需每周部署 1 次。分离可以让 A 不用等待 B。

4.3. 基础设施的故障容忍能力

# docker-compose.yml - 演示微服务最小基础设施复杂度
services:
  order-service:
    image: order-service:1.0
    depends_on:
      - inventory-service
      - payment-service
      - service-registry
  inventory-service:
    image: inventory-service:1.0
  payment-service:
    image: payment-service:1.0
  service-registry:
    image: eureka-server:1.0
  api-gateway:
    image: spring-cloud-gateway:1.0

仅仅有 3 个业务服务,你就需要额外的服务注册表和 API 网关。乘以 20 个实际服务,活动部件数量按指数增长——需要足够强大的 DevOps 团队来运维 Kubernetes、分布式日志(ELK)、追踪(Jaeger/Zipkin)。

注意: 如果公司还没有人精通容器编排和可观测性栈,微服务会把每个事件变成一场“大海捞针”式的多个服务日志查询。

5. 模块化单体:被忽视的中间选择

模块化单体是单体,但代码被组织成独立的模块,通过明确的接口进行通信,没有跨模块的数据库依赖。

// Package: com.nonladev.shop.order
public interface InventoryPort {
    void reserve(String productId, int quantity);
}

// Package: com.nonladev.shop.inventory
@Service
public class InventoryService implements InventoryPort {
    @Override
    public void reserve(String productId, int quantity) {
        // 业务逻辑,访问inventory模块的专属数据库
    }
}

OrderService 只依赖于 InventoryPort 接口,不知道 inventory 模块的实现细节。因此,如果以后需要将 inventory 分离为独立服务,你只需将 InventoryPort 的实现替换为 HTTP 客户端——OrderService 中的调用代码不需要改变。

这正是许多大型系统(如早期的 Shopify、GitHub)成功采用的“先从单体开始,逐步分离”策略。

6. 测试:衡量从进程内调用到网络调用的成本

下面的测试示例展示了延迟的差异——这是一个在决定分离服务前需要实际测量的因素。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <scope>test</scope>
</dependency>
class OrderServiceLatencyTest {

    @Test
    void inProcessCallShouldBeFasterThanRemoteCall() {
        InventoryPort inProcess = (productId, qty) -> { /* no-op */ };
        long startInProcess = System.nanoTime();
        inProcess.reserve("P-1", 1);
        long inProcessDuration = System.nanoTime() - startInProcess;

        InventoryPort remoteSimulated = (productId, qty) -> {
            try {
                Thread.sleep(20); // 模拟网络往返延迟~20ms
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };
        long startRemote = System.nanoTime();
        remoteSimulated.reserve("P-1", 1);
        long remoteDuration = System.nanoTime() - startRemote;

        assertThat(remoteDuration).isGreaterThan(inProcessDuration);
    }
}

这个测试不测量你的基础设施的真实性能,但说明了本质:每次 Order 通过网络调用 Inventory,你都增加了几到几十毫秒。如果一个请求需要调用 5 个服务的链,累积延迟可能会超过 SLA——这是在纸上绘制架构图时经常被忽视的成本。

7. 何时不应该选择微服务

  • 少于 10 人的团队: 多个服务的运维成本(CI/CD、监控、轮流待命)超过了分离的收益。
  • 域边界未定: 如果业务不断变化(初创公司寻找产品-市场契合度的阶段),服务边界会经常出错,分离后又要合并——工作量翻倍。
  • 缺乏可观测性: 没有分布式追踪、集中式日志,跨服务调试几乎不可能。
  • 需要强一致性的事务(如核心银行系统):保持在单个事务边界(单体)内比实现复杂的 saga 更安全。

相反,微服务值得投资的情况是:多个独立团队、需要分别扩展不同部分(如搜索服务需要比订单服务更多 CPU)、或每个域需要不同技术(用 Node.js 处理实时聊天,用 Java 处理账单)。

8. 结论

没有“绝对正确”的架构——只有适合你当前团队规模、发布速度和运维能力的架构。如果不确定,可以从模块化单体开始:保持域之间的接口清晰,避免数据库交叉依赖,这样当真正需要分离时(由于扩展或团队增长),你只需要改变实现,而不是重写整个系统。微服务是解决组织和扩展问题的工具,而不是架构目标本身。

microservicesmonolithsystem-designspring-bootarchitecture