微服务架构迁移与云原生服务编排 - 第1节

一、单体架构的黄金时代:什么是单体调度

说起单体架构,我脑子里第一个画面是十年前刚入行时做的一个电商项目。那时候整个团队就十几个人,一个应用包搞定所有功能——用户、商品、订单、支付,全在一个进程里跑。部署?一个 Tomcat 搞定。上线?半夜发个包就行。

说白了,单体调度就是所有业务逻辑都在同一个应用进程里,由应用自身来管理请求的流转和资源的分配。你想想看,一个请求进来,Controller 调 Service,Service 调 DAO,DAO 查数据库,整个过程都在一个 JVM 堆栈里完成。没有网络开销,没有序列化成本,也没有服务间调用的超时重试。

核心定义:单体调度是指应用内部通过函数调用或方法调用的方式,在单一进程内完成请求的处理、资源的分配和任务的编排。调度逻辑与业务代码紧密耦合,不存在跨进程的通信开销。

我记得当时我们团队有个老哥,特别喜欢在 Service 层里直接 new 线程池做异步任务。嗯,这在单体时代其实挺常见的——反正都在一个进程里,线程池共享内存,数据一致性靠数据库事务保证,简单粗暴但有效。

单体调度的典型特征

  • 进程内调用:所有模块通过方法调用通信,没有网络 IO
  • 共享内存:全局变量、缓存、线程池都在同一个 JVM 里
  • 统一事务:一个 @Transactional 注解就能搞定跨表事务
  • 集中式配置:数据库连接、缓存配置、MQ 配置全在一个 application.yml 里

我在项目中遇到过最夸张的一个单体应用,war 包解压后有 800 多兆。启动要 5 分钟,但跑起来确实稳。为什么?因为没有网络抖动,没有服务降级,没有熔断限流这些破事。

单体应用的优势与痛点

优势:为什么它能统治十年

别一提到单体就摇头。我做了这么多年架构,见过太多一上来就拆微服务,结果把自己拆死的团队。单体架构能活这么久,是有道理的。

优势 说明 我的体会
开发效率高 IDE 里一个项目搞定所有代码,调试方便 我最快一次从零搭单体电商只用了一周
部署简单 一个 war 包扔到 Tomcat 就完事 运维同学最喜欢单体,不用配网关和注册中心
性能损耗小 进程内调用,没有网络开销 单机 QPS 轻松上千,延迟在毫秒级
事务一致性强 本地事务 + 数据库锁,ACID 天然支持 支付和库存扣减在一个事务里,不用操心分布式事务
调试方便 一个断点就能跟踪完整请求链路 出了问题,本地复现一下就能定位

我的建议:如果你的团队在 10 人以内,业务复杂度不高,别盲目上微服务。单体架构的简单性本身就是一种生产力。我见过太多 5 个人的团队,硬要拆 8 个微服务,结果每天光联调就花半天。

痛点:为什么它撑不住了

但是,当业务规模上来之后,单体的毛病就藏不住了。我亲身经历过一个电商系统,日活从 10 万涨到 100 万,单体架构开始各种「闹脾气」。

  1. 编译部署慢:800 兆的 war 包,编译 10 分钟,部署 5 分钟。改一行代码,整个应用都要重新发布。
  2. 团队协作困难:20 个人同时改一个代码库,合并冲突能让你崩溃。我记得有一次,两个同事同时改了订单模块的同一个文件,合并花了整整一下午。
  3. 扩展粒度太粗:订单模块扛不住了,你想加机器?对不起,整个应用都得一起扩。用户模块明明没压力,也得跟着吃资源。
  4. 技术栈锁定:整个项目用 Java,你就不能在某些模块用 Go 或 Python。想换数据库?所有模块都得跟着换。
  5. 故障隔离差:一个模块的内存泄漏,能把整个应用拖垮。我遇到过用户模块的一个死循环,直接把订单和支付模块一起搞挂了。

避坑指南:我曾经接手过一个单体电商项目,代码里到处都是 ThreadLocal 传参。一开始觉得方便,后来发现请求上下文经常串数据。排查了三天,最后发现是线程池复用导致 ThreadLocal 没清理。嗯,从那以后我对共享内存就特别警惕。

一个典型的单体电商系统拆解

光说不练假把式。咱们来看一个真实的单体电商系统长什么样。这是我几年前参与过的一个项目,虽然现在回头看有很多槽点,但当时确实撑起了千万级的 GMV。

系统架构总览

先上一张架构图,让大家对整体有个感觉。

典型单体电商系统架构 客户端层 Web 浏览器 | 移动 App | 小程序 | H5 接入层 Nginx | 统一网关 | 鉴权拦截器 | 限流 业务层(单体应用核心) 用户模块 注册/登录/权限 商品模块 SPU/SKU/库存 订单模块 下单/状态流转 支付模块 支付/退款/对账 营销 优惠券 数据层 MySQL | Redis | Elasticsearch | RabbitMQ 所有业务模块共享同一个数据库实例和缓存集群,通过本地事务保证数据一致性

模块职责与调度关系

这个系统里,每个模块的职责其实挺清晰的。我挑几个重点模块说说。

模块 核心职责 调度方式 典型问题
用户模块 注册、登录、权限校验、地址管理 直接调用 UserService 方法 Session 共享问题
商品模块 SPU/SKU 管理、库存维护、分类搜索 调用 ProductService + 本地缓存 库存超卖
订单模块 下单、状态流转、超时取消 调用 OrderService + 定时任务 状态不一致
支付模块 支付回调、退款、对账 异步回调 + 本地事务 掉单问题
营销模块 优惠券发放、满减活动、积分 硬编码在订单流程中 活动规则变更频繁

一次完整的下单流程

咱们来走一遍用户下单的完整流程,看看单体调度是怎么工作的。

// 伪代码:单体电商下单流程
@Transactional
public Order createOrder(Long userId, Long skuId, Integer quantity) {
    // 1. 校验用户
    User user = userService.getById(userId);
    if (user == null || user.getStatus() == 0) {
        throw new BusinessException("用户不存在或已禁用");
    }

    // 2. 查询商品
    Sku sku = productService.getSkuById(skuId);
    if (sku == null || sku.getStock() < quantity) {
        throw new BusinessException("库存不足");
    }

    // 3. 计算价格(含营销优惠)
    BigDecimal price = marketingService.calculatePrice(sku, quantity, userId);

    // 4. 扣减库存
    productService.deductStock(skuId, quantity);

    // 5. 创建订单
    Order order = new Order();
    order.setUserId(userId);
    order.setSkuId(skuId);
    order.setQuantity(quantity);
    order.setAmount(price);
    order.setStatus(OrderStatus.PENDING_PAYMENT);
    orderDao.insert(order);

    // 6. 发送消息(异步)
    mqService.send("order.created", order.getId());

    return order;
}

你看,整个过程都在一个事务里。从用户校验到库存扣减,再到订单创建,全部是本地方法调用。没有网络超时,没有分布式事务,没有服务降级。这就是单体调度的魅力——简单、可靠、高效。

一个小细节:注意第 6 步的 MQ 发送。我习惯把 MQ 放在事务的最后,而且用的是同一个数据库事务。如果 MQ 发送失败,整个事务回滚,订单和库存都不会被提交。这在单体里很容易做到,但到了微服务时代,这种「本地事务 + 消息」的模式就变成了分布式事务的难题。

单体调度的隐形成本

不过,这种「一个事务搞定一切」的爽感,随着业务增长会慢慢变成负担。我总结了几点单体调度的隐形成本:

  • 长事务风险:一个下单流程涉及 5-6 个模块,事务时间可能超过 1 秒。高并发下,数据库连接池很容易被打满。
  • 代码耦合严重:营销模块的代码直接写在订单 Service 里。改个优惠规则,订单模块也得重新部署。
  • 资源争抢:用户模块的慢查询会拖慢整个应用的数据库连接池。我记得有一次,用户模块的一个全表扫描,直接把订单模块的查询也拖垮了。
  • 扩展瓶颈:订单模块需要更多计算资源?对不起,整个应用都得一起扩。用户模块和商品模块也跟着吃资源,浪费严重。

我曾经踩过的坑:有一次线上大促,订单量暴涨。单体应用的数据库连接池被打满,所有请求都在等数据库连接。更惨的是,用户模块的登录请求也在等同一个连接池,导致新用户根本进不来。嗯,那次事故之后,我们才开始认真考虑拆分的事情。