实战案例四:Web后端API重构 - 从单体到微服务的渐进式重构

说实话,这个案例是我在课程里最想聊的一个。为什么?因为太多团队一上来就说「我们要搞微服务」,结果拆到一半发现业务跑不动了,回退又来不及。我见过太多这样的惨案了。

今天这个案例,讲的是我们如何把一个臃肿的单体API,一步步拆成微服务架构。注意关键词——渐进式。不是一夜之间推倒重来,而是像做手术一样,一刀一刀来。

背景:这个单体到底有多「单」?

我当时接手的是一个电商后端的核心API服务。代码量大概20万行,部署在一个巨型WAR包里。每次发布,全团队都得熬夜。你想想看,改一行代码,整个服务都得重启。

问题清单是这样的:

  • 耦合严重:订单、用户、商品、支付,全在一个进程里
  • 数据库共享:所有模块共用同一个MySQL库,一个慢查询拖垮所有人
  • 部署困难:一次发布要跑半小时的测试,上线后出问题回滚代价极大
  • 团队冲突:前端改个接口,后端得排期两周,因为大家都在同一个代码库里改

嗯,说白了,这就是典型的「大泥球」架构。我当时的直觉是——必须拆,但不能蛮干。

第一步:先画边界,再动刀子

我个人习惯,重构之前先画图。不是画代码结构图,而是画业务边界图。你得搞清楚,哪些功能是内聚的,哪些是频繁交互的。

核心原则:按业务领域拆分,而不是按技术分层拆分。

比如:订单领域、用户领域、商品领域、支付领域。每个领域有自己的数据、逻辑和接口。

我当时带着团队做了个简单的领域分析,画出了这张图:

阶段一:单体架构 单体API服务 ┌─ 订单模块 ├─ 用户模块 ├─ 商品模块 └─ 支付模块 共享数据库: MySQL 部署: 单WAR包 渐进式重构 阶段二:微服务架构 订单服务 独立DB + API 用户服务 独立DB + API 商品服务 独立DB + API 支付服务 独立DB + API API 网关 (统一入口)

这张图其实很关键。它告诉我们:不要试图一次拆完。先拆最独立的模块,比如用户服务。用户模块和订单模块的交互最少,拆起来风险最低。

第二步:从「数据分离」开始

我记得当时第一个动手的是用户服务。为什么选它?因为用户数据的读写频率高,但和其他模块的耦合度相对低。

具体做法是这样的:

  1. 先拆数据库:把用户表从主库迁移到独立的用户库。注意,这一步要双写。老库和新库同时写,保证数据一致。
  2. 再拆代码:把用户相关的Controller、Service、DAO抽出来,单独部署成一个服务。
  3. 最后切流量:通过网关配置,把用户相关的请求路由到新服务上。

我的经验:数据分离是最容易出问题的环节。我曾经因为没做好双写校验,导致用户数据丢失了2小时。从那以后,我要求团队必须做「数据校验脚本」,每天跑一次对比老库和新库的数据差异。

代码层面,我们做了这样的改造。这是原来的单体代码:

// 单体时代:所有逻辑混在一起
@RestController
public class OrderController {
    @Autowired
    private UserService userService;
    @Autowired
    private OrderService orderService;
    @Autowired
    private ProductService productService;

    @PostMapping("/order/create")
    public Result createOrder(@RequestBody OrderRequest req) {
        User user = userService.getUser(req.getUserId());
        Product product = productService.getProduct(req.getProductId());
        // ... 各种校验和逻辑
        return orderService.createOrder(user, product);
    }
}

拆完之后,每个服务只关心自己的事:

// 微服务时代:用户服务只处理用户
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public Result getUser(@PathVariable Long id) {
        return userService.getUser(id);
    }
}

// 订单服务通过RPC调用用户服务
@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired
    private OrderService orderService;
    @Autowired
    private UserServiceClient userClient; // Feign客户端

    @PostMapping("/create")
    public Result createOrder(@RequestBody OrderRequest req) {
        User user = userClient.getUser(req.getUserId());
        // 订单服务只处理订单逻辑
        return orderService.createOrder(user, req);
    }
}

你发现没有?每个服务的代码量少了,职责清晰了。但代价是——网络调用增加了。原来一次本地方法调用,现在变成了远程RPC。延迟从1ms变成了10ms。

注意:微服务不是银弹。拆完之后,你得面对分布式事务、服务发现、配置中心、链路追踪等一系列新问题。我见过最夸张的案例,一个团队拆了20个服务,结果运维成本翻了10倍。

第三步:引入API网关,统一入口

服务拆完了,客户端怎么调用?总不能每个前端都记住10个服务的地址吧?这时候就需要API网关。

我们选用了Spring Cloud Gateway,配置很简单:

# 网关路由配置
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/api/product/**

网关的好处是:

  • 统一鉴权:在网关层做token校验,不用每个服务都写一遍
  • 限流熔断:防止某个服务被流量冲垮
  • 协议转换:比如把外部HTTP请求转成内部gRPC调用

我个人习惯,网关层只做「路由+安全+限流」,不要放业务逻辑。一旦网关里塞了业务代码,它就会变成新的「单体瓶颈」。

第四步:处理分布式事务

这是最头疼的部分。单体时代,一个事务搞定的事情,拆成微服务后,得跨服务协调。

举个例子:下单扣库存。原来是这样:

@Transactional
public void createOrder(Order order) {
    orderDao.insert(order);
    inventoryDao.decrease(order.getProductId(), order.getQuantity());
}

拆完之后,订单服务和库存服务各自有独立数据库。怎么办?

我们采用了最终一致性方案

  1. 订单服务先创建订单,状态设为「待支付」
  2. 发送消息到MQ,通知库存服务扣减库存
  3. 库存服务扣减成功后,发送回执消息
  4. 如果库存不足,订单服务收到回执后取消订单

避坑指南:我曾经因为消息丢失,导致用户下单成功但库存没扣。后来加了「本地消息表」机制——先写数据库,再发消息。如果消息发送失败,有定时任务重试。

效果:重构后的变化

重构完成后,我们做了个对比:

指标 重构前(单体) 重构后(微服务)
部署时间 30分钟 5分钟(每个服务独立部署)
单次发布影响范围 全服务 仅影响单个服务
团队协作效率 经常代码冲突 各团队独立开发
系统可用性 99.5% 99.9%(故障隔离)
接口响应时间 50ms(本地调用) 80ms(含网络开销)

你看,响应时间确实变长了,但换来的是可维护性和可扩展性的大幅提升。说白了,这就是个trade-off。如果你的业务不需要频繁迭代,单体可能更合适。

总结:渐进式重构的要点

  • 不要一步到位:先拆最独立的模块,积累经验
  • 数据先行:先分离数据库,再分离代码
  • 做好双写和校验:数据一致性是底线
  • 引入网关:统一入口,降低客户端复杂度
  • 接受最终一致性:分布式事务没有银弹

嗯,这个案例就聊到这儿。重构不是目的,让业务跑得更稳、团队开发更高效才是。你想想看,如果每次发布都提心吊胆,那再新的技术也救不了你。


公众号:蓝海资料掘金营,微信deep3321