什么时候该从单体走向微服务

很多团队一上来就想做微服务,觉得"高大上"。但真实世界里,微服务解决的是组织和协作的问题,不是技术先进性的问题。当一个单体项目出现下面这些信号时,拆分才开始有收益:

  • 不同模块的发布频率完全不同——用户中心一周发三次,报表模块半年不动一次,却要跟着一起打包上线。
  • 一个模块的性能问题会拖垮全站——报表查询把数据库连接池占满,下单接口跟着超时。
  • 团队超过十人,所有人都在同一个代码库上改代码,合并冲突频繁,谁也不敢动老模块。

如果你的项目还在三五个人、十万行代码以内,老老实实用单体模块化(Maven 多模块)就够了。微服务把部署、监控、配置、链路追踪的复杂度全部搬了上来,没有对应的团队规模和运维能力,拆出来只会更痛。

微服务不是目的,它是"让不同团队能独立交付价值"的手段。如果一个人维护三个服务,那不是微服务,是给自己找罪受。
本文核心知识点
  • 注册发现服务启动时把自己的地址注册到 Nacos,调用方从 Nacos 拿地址列表。
  • 配置中心所有环境配置集中管理,改配置不用重新打包发版。
  • Feign用接口注解声明式地调用其他服务,不用手写 HTTP 客户端。
  • 网关统一入口做路由、鉴权、限流,后端服务不直接暴露。
微服务架构示意:蓝色渐变背景中央一个注册中心节点,周围环绕多个数据库圆柱体服务节点,由发光连线织成分布式网络
注册中心是微服务的通讯录:每个服务启动时报到,调用方查通讯录找到对方地址。

微服务架构的三件基础设施

一个最小可用的微服务体系,至少需要三样东西:注册中心(服务在哪)、配置中心(服务怎么配)、网关(外部怎么进来)。Nacos 一个产品同时承担前两个角色,这也是它在国内 Java 生态里流行的原因——少装一个组件就少一份运维成本。

组件解决的问题没有它会怎样
注册中心动态发现服务实例地址调用方写死 IP,服务扩容要改代码
配置中心集中管理多环境配置每个环境改一遍 application.yml,容易漏
API 网关统一路由、鉴权、限流前端要对接 N 个服务地址,鉴权逻辑散落各处
负载均衡一个服务多实例时分发请求只能打到固定一台,挂了就全挂

Spring Cloud Alibaba 生态把这些都封装成了 starter 依赖:引入 spring-cloud-starter-alibaba-nacos-discovery 就有了注册发现,引入 spring-cloud-starter-alibaba-nacos-config 就有了配置中心,引入 spring-cloud-starter-openfeign 就有了声明式调用。业务代码里几乎感知不到这些基础设施的存在。

Nacos 部署与服务注册

开发阶段最快的方式是直接下载 Nacos 官方发布包,单机模式启动:

# 下载解压后,进入 bin 目录,单机模式启动
# Windows
startup.cmd -m standalone
# Linux / macOS
sh startup.sh -m standalone

# 启动后访问控制台
# http://localhost:8848/nacos  默认账号 nacos / nacos

服务端跑起来之后,在 Spring Boot 项目里加依赖和配置,服务启动时就会自动注册上去:

<!-- pom.xml -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

# application.yml
spring:
  application:
    name: user-service          # 服务名,其他服务通过这个名字找你
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848   # Nacos 地址

启动两个 user-service 实例(改不同端口),打开 Nacos 控制台的"服务列表",就能看到 user-service 下面挂着两个健康实例。这时候调用方不需要知道任何 IP,只要知道服务名 user-service,负载均衡组件就会从 Nacos 拉到实例列表,自己挑一个发请求。

  • 健康检查:Nacos 会定期探测实例,挂掉的实例自动摘除,调用方不会再打过去。
  • 权重:每个实例可以设权重,性能好的机器多接流量,不用手动改配置。
  • 命名空间:用 namespace 区分开发、测试、生产环境,避免互相污染。

配置中心:把配置从代码里挪出来

没有配置中心的时候,数据库密码、接口地址、开关标志散落在各个服务的 application.yml 里,改一个开关要重新打包、重新部署所有服务。配置中心的思路是:配置统一存在 Nacos 上,服务启动时拉取,运行时还能动态刷新。

# 加配置中心依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

# bootstrap.yml(配置中心的地址要在最早加载)
spring:
  application:
    name: order-service
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml     # 配置文件格式

在 Nacos 控制台新建一个 Data ID 为 order-service.yaml 的配置,写入业务参数。服务启动时会自动拉这份配置,@Value 或 @ConfigurationProperties 注入的变量就能读到。需要动态刷新的配置类上加 @RefreshScope,Nacos 上一改,服务里的变量自动更新,不用重启。

@RestController
@RefreshScope
public class FeatureController {
    @Value("${feature.promotion.enabled:false}")
    private boolean promotionEnabled;

    @GetMapping("/feature")
    public String feature() {
        return "促销开关:" + promotionEnabled;
    }
}

这套机制在灰度发布和紧急开关场景特别有用:大促前临时关掉一个非核心功能,在 Nacos 控制台改一个值,所有实例秒级生效,不用重新发版。

Feign 声明式服务调用

服务 A 要调服务 B,最原始的做法是 RestTemplate 手写 HTTP 请求、拼 URL、解析 JSON。Feign 把这件事变成了"写一个接口、加几个注解",像调本地方法一样调远程服务。

// 1. 启动类开启 Feign
@EnableFeignClients
@SpringBootApplication
@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

// 2. 定义一个调用 user-service 的接口
@FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping("/users/{id}")
    UserDTO getUser(@PathVariable("id") Long id);
}

// 3. 在业务里直接注入使用
@Service
public class OrderService {
    @Autowired
    private UserClient userClient;

    public Order createOrder(Long userId) {
        UserDTO user = userClient.getUser(userId);   // 像调本地方法
        // 这里写下单的具体业务逻辑
        return new Order(userId, user.getName());
    }
}

Feign 底层帮你做了三件事:根据服务名从 Nacos 拿到实例列表、做负载均衡选一个、把接口方法映射成 HTTP 请求。你完全不需要知道 user-service 部署在哪台机器、跑在哪个端口。

配合 Spring Cloud LoadBalancer,Feign 默认就是轮询负载均衡;配合 Sentinel,还能加超时熔断——某个服务挂了,调用方不会一直等,而是快速失败走降级逻辑。这些都是在配置文件里开关,业务代码一行不用改。