【洛水石】 | 架构进阶 | Java架构师必读

图1:单体架构到微服务的四阶段演进路线图

很多团队在业务快速增长后,突然发现原来的单体系统撑不住了。但盲目拆微服务不是银弹,拆错了反而让系统更脆弱。本文从真实案例出发,系统梳理单体→微服务的演进路径、拆分策略、技术选型和常见踩坑。

一、先搞清楚:你真的需要微服务吗?

1.1 微服务不是终点,而是手段

微服务解决的核心问题:

| 问题 | 微服务如何解决 |

|------|---------------|

| 单体部署风险高(改一处全挂) | 独立部署,故障隔离 |

| 团队协作瓶颈(多人改同一仓库) | 按服务拆分,团队独立迭代 |

| 局部扩容困难(只有一个Pod) | 按服务独立扩缩容 |

| 技术栈绑死(全是Java) | 多语言混用 |

1.2 但微服务也带来了新问题

  - **网络延迟**:原来是本地调用,现在变成RPC

  - **分布式事务**:跨服务的数据一致性保证

  - **服务治理**:注册、发现、熔断、限流一套都要搞

  - **运维复杂度**:从1个应用变成10+个服务,日志链路追踪

  - **开发成本上升**:本地开发时需要跑多个服务

1.3 什么时候不应该上微服务

Martin Fowler:"微服务首先要解决的第一个问题,是如何识别微服务的边界。"

以下情况不建议:

  - 团队规模 < 10人

  - 业务还不稳定,边界模糊

  - 日活用户 < 1万,没有明显性能瓶颈

  - 团队没有K8s/Docker运维经验

结论:先把单体做好,再谈拆分。

二、演进路线图:四个阶段

架构演进不应该一步到位,应该按业务增长节奏分阶段推进:

阶段一:单体 + 垂直扩展(10万日活以内)
   ↓ 瓶颈:数据库连接数、CPU/内存、模块耦合
阶段二:单体 + 分层优化(引入缓存、读写分离)
   ↓ 瓶颈:发布风险、团队协作、局部扩容
阶段三:模块化单体 → 服务化(业务边界清晰化)
   ↓ 瓶颈:跨模块调用变多,独立部署需求强
阶段四:微服务体系(完整治理栈)

三、第一阶段:优化单体,把地基打牢

3.1 数据库优化

在系统规模不大时,很多问题可以靠数据库优化解决:

-- 1. 合理建立索引
ALTER TABLE orders ADD INDEX idx_user_status(user_id, status);
ALTER TABLE orders ADD INDEX idx_created_at(created_at);

-- 2. 慢查询监控
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;  -- 超过1s记录慢日志

-- 3. 读写分离(通过代理层)
-- 主库:写操作
-- 从库:读操作(配合 MyBatis-Plus @DS 注解)

3.2 引入缓存层

// Redis缓存典型场景
@Service
public class ProductService {

    @Cacheable(value = "product", key = "#productId",
               unless = "#result == null")
    public ProductVO getProduct(Long productId) {
        return productMapper.selectById(productId);
    }

    @CacheEvict(value = "product", key = "#product.id")
    public void updateProduct(Product product) {
        productMapper.updateById(product);
    }
}

3.3 应用层垂直扩展

# docker-compose.yml - 单体应用多实例 + Nginx负载均衡
version: '3'
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf

  app1:
    image: myapp:latest
    environment:
      - SERVER_PORT=8080

  app2:
    image: myapp:latest
    environment:
      - SERVER_PORT=8081

四、第二阶段:模块化单体,为拆分做准备

4.1 为什么要先做模块化

直接把耦合的单体拆成微服务,100%翻车。正确姿势是:

先在单体内部做好模块边界,再分离部署。

4.2 DDD战略设计:识别限界上下文

以电商系统为例,用DDD方法识别业务域:

核心域(核心竞争力):
  └── 订单域:下单、支付、退款、售后
  └── 商品域:SKU管理、库存、定价

支撑域(业务支撑):
  └── 用户域:注册、登录、权限
  └── 营销域:优惠券、满减、积分

通用域(可复用):
  └── 消息通知:短信、邮件、推送
  └── 文件服务:图片、文件上传

4.3 包结构按模块划分

com.company.ecommerce/
├── order/                      # 订单模块
│   ├── domain/                 # 领域模型
│   │   ├── Order.java
│   │   └── OrderItem.java
│   ├── service/
│   │   └── OrderService.java
│   ├── repository/             # 数据访问(只访问本模块的表)
│   └── api/                    # 对外接口(仅通过接口调用)
│       └── OrderApi.java
├── product/                    # 商品模块
│   ├── domain/
│   ├── service/
│   └── api/
│       └── ProductApi.java
└── user/                       # 用户模块
    └── api/
        └── UserApi.java

关键约定:

  - 模块间**只允许通过 api 层接口调用**,禁止直接访问其他模块的 Service 或 Mapper

  - 每个模块**只访问自己的数据库表**,禁止跨模块 JOIN

4.4 用门面模式隔离内部实现

// 订单模块对外暴露的接口(api层)
public interface OrderApi {
    OrderDTO createOrder(CreateOrderCommand cmd);
    OrderDTO getOrder(Long orderId);
    void cancelOrder(Long orderId, String reason);
}

// 商品模块查询接口
public interface ProductApi {
    ProductDTO getProduct(Long productId);
    boolean checkAndDeductStock(Long skuId, Integer count);
}

// 订单服务通过接口调用商品,不直接依赖ProductMapper
@Service
public class OrderServiceImpl implements OrderService {

    private final ProductApi productApi;  // 接口注入

    public OrderDTO createOrder(CreateOrderCommand cmd) {
        // 通过接口查询商品,将来改为RPC调用只需换实现
        ProductDTO product = productApi.getProduct(cmd.getProductId());
        boolean deducted = productApi.checkAndDeductStock(
            cmd.getSkuId(), cmd.getCount());
        // ...
    }
}

五、第三阶段:拆出第一个微服务

5.1 拆分顺序:先拆通用服务,后拆核心服务

| 拆分顺序 | 原因 |

|---------|------|

| **第一批:无状态通用服务**(文件、短信) | 边界清晰,不涉及事务 |

| **第二批:读多写少的查询服务**(商品展示) | 可缓存,容错简单 |

| **第三批:相对独立的业务模块**(用户中心) | 边界清晰,依赖少 |

| **最后:核心交易链路**(订单、支付) | 最复杂,留到团队有经验后再拆 |

5.2 数据库如何拆

一个服务一个库,这是铁律:

-- 拆分前(单库)
CREATE DATABASE ecommerce;
USE ecommerce;
-- user, product, order, inventory 表全在一起

-- 拆分后(分库)
CREATE DATABASE ecommerce_user;      -- 用户库
CREATE DATABASE ecommerce_product;   -- 商品库
CREATE DATABASE ecommerce_order;     -- 订单库
CREATE DATABASE ecommerce_inventory; -- 库存库

拆库迁移三步法:

步骤1:双写阶段
  新代码同时写旧库和新库,以旧库为主

步骤2:验证阶段
  对比新旧库数据一致性,修复差异

步骤3:切换阶段
  新库读写,旧库废弃
  保留旧库一段时间作为兜底

5.3 服务间通信选型

| 方式 | 框架 | 适用场景 |

|------|------|---------|

| **REST HTTP** | OpenFeign | 简单调用,对延迟要求不高 |

| **RPC** | Dubbo/gRPC | 高频调用,性能优先 |

| **消息队列** | RocketMQ/Kafka | 异步解耦,最终一致性 |

// OpenFeign调用示例
@FeignClient(name = "product-service", fallbackFactory = ProductFallback.class)
public interface ProductFeignClient {

    @GetMapping("/internal/product/{id}")
    ProductDTO getProduct(@PathVariable Long id);

    @PostMapping("/internal/stock/deduct")
    Boolean deductStock(@RequestBody DeductStockRequest request);
}

// 降级处理
@Component
public class ProductFallback implements FallbackFactory<ProductFeignClient> {
    @Override
    public ProductFeignClient create(Throwable cause) {
        return new ProductFeignClient() {
            @Override
            public ProductDTO getProduct(Long id) {
                log.error("商品服务降级,id={}", id, cause);
                return null;  // 或返回缓存数据
            }

            @Override
            public Boolean deductStock(DeductStockRequest request) {
                log.error("库存扣减降级,走MQ异步补偿", cause);
                return false;
            }
        };
    }
}

六、第四阶段:完整微服务治理体系

6.1 服务注册与发现

# application.yml - Nacos配置
spring:
  cloud:
    nacos:
      discovery:
        server-addr: nacos:8848
        namespace: prod
        group: DEFAULT_GROUP
      config:
        server-addr: nacos:8848
        file-extension: yaml
        shared-configs:
          - dataId: common-config.yaml
            refresh: true

6.2 网关统一入口

# Gateway路由配置
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 100
                redis-rate-limiter.burstCapacity: 200

        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/api/product/**
          filters:
            - StripPrefix=1

6.3 Sentinel 熔断限流

@RestController
public class OrderController {

    @GetMapping("/order/{id}")
    @SentinelResource(
        value = "getOrder",
        blockHandler = "getOrderBlocked",    // 触发限流/熔断时
        fallback = "getOrderFallback"        // 抛出异常时
    )
    public Result<OrderVO> getOrder(@PathVariable Long id) {
        return Result.ok(orderService.getOrder(id));
    }

    // 限流降级方法
    public Result<OrderVO> getOrderBlocked(Long id, BlockException ex) {
        return Result.fail(429, "系统繁忙,请稍后重试");
    }

    // 异常降级方法
    public Result<OrderVO> getOrderFallback(Long id, Throwable t) {
        log.error("订单查询异常,id={}", id, t);
        return Result.fail(500, "服务暂时不可用");
    }
}

6.4 链路追踪:SkyWalking

<!-- pom.xml -->
<dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-trace</artifactId>
    <version>9.2.0</version>
</dependency>

// 自定义埋点
@Service
public class OrderService {

    @Trace(operationName = "createOrder")
    public OrderDTO createOrder(CreateOrderCommand cmd) {
        ActiveSpan.tag("userId", cmd.getUserId().toString());
        ActiveSpan.tag("productId", cmd.getProductId().toString());
        // 业务逻辑...
    }
}

七、跨服务事务:分布式事务解决方案

微服务最大的难题是分布式事务。详见专题文章,这里给出选型建议:

场景一:强一致性要求(金融转账)
  → Seata AT 模式(自动补偿,适合简单CRUD)
  → Seata TCC 模式(手动补偿,适合高并发)

场景二:最终一致性(订单创建→积分发放)
  → 本地消息表 + RocketMQ 事务消息
  → 幂等性保证:唯一消息ID + 数据库唯一索引

场景三:长流程业务(物流、退款)
  → Saga 模式(状态机驱动,每步记录)

八、常见踩坑汇总

坑1:服务拆太细,反成"分布式单体"

拆了20个服务,每个服务只有2个接口,调用链深达10层。

原则:按业务能力(Business Capability)拆,不是按技术层拆。

坑2:共享数据库

❌ order_service 和 product_service 都连同一个 ecommerce 库
✅ 每个服务有自己的数据库,通过API调用获取其他服务数据

坑3:忽略服务启动顺序

# docker-compose 依赖设置
services:
  order-service:
    depends_on:
      nacos:
        condition: service_healthy
      mysql:
        condition: service_healthy

坑4:没有熔断,雪崩效应

一个服务变慢 → 调用方线程积压 → 调用方也变慢 → 整个系统崩溃

// 必须配置超时 + 熔断
feign:
  client:
    config:
      default:
        connectTimeout: 1000    # 1s连接超时
        readTimeout: 3000       # 3s读超时
  circuitbreaker:
    enabled: true               # 开启熔断

坑5:日志没有TraceId,排查困难

// MDC传递TraceId
public class TraceIdFilter implements Filter {
    @Override
    public void doFilter(ServletRequest req, ...) {
        String traceId = ((HttpServletRequest)req).getHeader("X-Trace-Id");
        if (StringUtils.isBlank(traceId)) {
            traceId = UUID.randomUUID().toString().replace("-","");
        }
        MDC.put("traceId", traceId);
        // 传递给下游
        chain.doFilter(req, resp);
        MDC.clear();
    }
}

九、演进检查清单

判断是否该拆分

  - [ ] 团队规模超过10人,且有独立负责不同业务模块的子团队

  - [ ] 部署频率高,一处改动影响全局发布

  - [ ] 某个模块(如商品搜索)有独立的扩容需求

  - [ ] 已完成模块化单体改造,模块边界清晰

拆分前准备

  - [ ] 建立分布式链路追踪(SkyWalking/Zipkin)

  - [ ] 建立统一日志中心(ELK)

  - [ ] 制定API接口规范

  - [ ] 建立服务注册中心(Nacos)

  - [ ] 制定回滚方案

拆分中注意

  - [ ] 先拆通用服务,再拆核心服务

  - [ ] 数据库随服务一起拆,不共享数据库

  - [ ] 服务间通信使用接口,便于切换

  - [ ] 每个服务单独部署,有独立CI/CD流水线

十、总结

| 阶段 | 关键动作 | 技术栈 |

|------|---------|--------|

| 单体优化 | 索引、缓存、读写分离 | MySQL、Redis、Nginx |

| 模块化单体 | DDD分模块、接口隔离 | Spring Boot模块化 |

| 服务拆分 | 按业务域拆、数据库独立 | Spring Cloud、Feign |

| 完整微服务 | 注册/发现/熔断/链路追踪 | Nacos、Sentinel、SkyWalking |

架构演进没有银弹,按业务阶段走,每一步都要有充分的理由。别因为"微服务流行"就拆服务,别因为"单体OUT了"就贬低单体。

更多推荐