什么时候该从单体走向微服务
很多团队一上来就想做微服务,觉得"高大上"。但真实世界里,微服务解决的是组织和协作的问题,不是技术先进性的问题。当一个单体项目出现下面这些信号时,拆分才开始有收益:
- 不同模块的发布频率完全不同——用户中心一周发三次,报表模块半年不动一次,却要跟着一起打包上线。
- 一个模块的性能问题会拖垮全站——报表查询把数据库连接池占满,下单接口跟着超时。
- 团队超过十人,所有人都在同一个代码库上改代码,合并冲突频繁,谁也不敢动老模块。
如果你的项目还在三五个人、十万行代码以内,老老实实用单体模块化(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,还能加超时熔断——某个服务挂了,调用方不会一直等,而是快速失败走降级逻辑。这些都是在配置文件里开关,业务代码一行不用改。
网关、容错与一条请求链路
所有外部请求不应该直接打到各个微服务,而是先经过一个 API 网关。网关负责路由转发、登录鉴权、限流熔断,后端服务只处理纯粹的业务逻辑。Spring Cloud Gateway 是 Spring 官方的网关实现,和 Nacos 集成后可以动态路由:
spring:
cloud:
gateway:
routes:
- id: user-route
uri: lb://user-service # lb 表示走负载均衡
predicates:
- Path=/api/users/**
- id: order-route
uri: lb://order-service
predicates:
- Path=/api/orders/**
一个完整的请求链路就变成了:浏览器 → 网关(鉴权、限流)→ order-service(下单)→ Feign 调 user-service(查用户)→ Feign 调 product-service(扣库存)→ 数据库。每一步都有自己的超时和熔断,某个环节挂了不会把整条链路拖死。
容错的关键是每个远程调用都要有超时和降级。Feign 默认超时通常太短,生产环境要根据业务 P99 耗时调整;降级方法要返回一个兜底值(比如"用户信息暂时查不到,先展示默认昵称"),而不是直接抛异常给前端。
微服务落地的常见误区
做微服务最容易踩的坑,几乎都是"技术先行、组织没跟上":
- 拆得太细:一个下单流程拆成了六个服务,一次请求要跨五个网络调用,延迟从 50ms 变成 500ms。拆分粒度要按业务边界,不是按技术分层。
- 共享数据库:拆了服务但所有服务连同一个数据库表,等于没拆,还多了一层网络开销。每个服务必须拥有自己的数据。
- 没有链路追踪:出了问题不知道是哪个服务慢、哪个服务错。上线前就要接好 Sleuth 或 SkyWalking,不要等出事再补。
- 配置中心当数据库用:Nacos 适合存配置和开关,不适合存高频变化的业务数据。
- 忽视部署复杂度:一个服务变十个服务,CI/CD、日志收集、监控告警都要跟着升级,不然运维会崩溃。
回到起点:微服务的价值在于让小团队独立交付,而不是把技术栈堆得花里胡哨。当你的团队还能高效协作于一个单体时,模块化的单体(Modular Monolith)反而是更聪明的选择。等业务真的涨到了那个临界点,再按本文的路径一步步拆,每一步都有清晰的收益可衡量。
好的微服务架构,是让一个新同事来了之后,只需要理解自己负责的那两三个服务,就能开始写代码。如果他要搞懂整个系统才能动手,那就是拆过头了。