微服务与 Spring Cloud
这一课你将能做到
- 说清单体架构的三个典型痛点,以及「什么时候才值得拆微服务」的判断标准
- 讲清 Spring Boot 与 Spring Cloud 的分工:一个管单个应用怎么跑,一个管一群应用怎么协作
- 动手搭一个 Maven 多模块工程,让 user-service 和 problem-service 作为两个独立进程同时运行
为什么要学
你在 Spring Boot 实战入门 已经能把一个完整 API 跑起来——本站(CodeForge)就是这样一个单体:Spring Boot + MySQL + Redis + Nginx,用户、题目、提交、判题全在一个 jar 里。单体在中小规模完全够用,但它有三个天花板:
- 部署耦合:改一行判题逻辑,整包重新发布;判题把 CPU 打满,用户登录接口也跟着变慢(同一进程抢资源)。
- 扩容粗粒度:明明只有判题模块压力大,却只能把整个应用复制 3 份,白白多耗一倍内存。
- 团队互锁:A 组改公共 DTO 导致 B 组编译失败;全站被锁死在同一种数据库方案、同一个框架版本上。
微服务就是把这些业务能力(用户、题目、判题)拆成独立进程、独立数据库、独立部署的小服务。但拆开之后立刻冒出新问题:服务地址怎么找?请求从哪个口进来?配置改了怎么同步?下游挂了怎么办?Spring Cloud 就是解决这一整套「协作问题」的工具箱。
面试视角:「为什么用微服务」几乎是 Java 后端必考题,回答的关键不是背概念,而是结合具体场景讲痛点与收益——这一课给你这套判断力和话术。
先建立图像
单体 vs 微服务,用本站举例:
【单体:一个 jar 全包】
浏览器 ──► Nginx ──► codeforge.jar:8080
├── UserController ──► MySQL(共用一个库)
├── ProblemController ──► 同一个库
└── JudgeController ──► 同一个库 + Redis
【微服务:按业务拆进程】
浏览器 ──► Nginx ──► Gateway:8080
├── user-service:8081 ──► user_db
├── problem-service:8082 ──► problem_db
└── judge-service:8083 ──► judge_db + Redis
(每个服务独立启动、独立发布、独立扩容;注册中心记录各服务地址)
一句话直觉:单体是一家什么菜都做的餐厅,微服务是把后厨按菜系分成几个档口,各自采购(数据库)、各自加人(扩容),传菜口统一对外(网关)。
跟着做
步骤 1:建立 Maven 父工程
微服务通常一个服务一个 Maven 模块。新建目录 cloud-demo,写父 pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>cloud-demo</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>user-service</module>
<module>problem-service</module>
</modules>
<properties>
<java.version>17</java.version>
<spring-boot.version>3.2.5</spring-boot.version>
<spring-cloud.version>2023.0.1</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
dependencyManagement 里的两个 BOM(依赖清单)统一管理全部子模块版本。注意:Spring Cloud 2023.0.x 必须搭配 Spring Boot 3.2.x/3.3.x,版本错配启动会报 NoSuchMethodError 之类莫名错误。
步骤 2:创建 user-service 模块
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>cloud-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>user-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
启动类 src/main/java/com/example/user/UserApplication.java:
package com.example.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
src/main/resources/application.yml:
server:
port: 8081
spring:
application:
name: user-service
package com.example.user.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public Map<String, Object> get(@PathVariable Long id) {
return Map.of("id", id, "nickname", "码农" + id, "from", "user-service");
}
}
步骤 3:创建 problem-service 模块
结构与上面完全一样,只改三处:artifactId 为 problem-service、端口 8082、spring.application.name 为 problem-service。接口返回题目:
package com.example.problem.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/problems")
public class ProblemController {
@GetMapping("/{id}")
public Map<String, Object> get(@PathVariable Long id) {
return Map.of("id", id, "title", "两数之和", "difficulty", "EASY");
}
}
步骤 4:同时跑两个进程验证
mvn spring-boot:run -pl user-service
mvn spring-boot:run -pl problem-service
分别访问 http://localhost:8081/users/1 和 http://localhost:8082/problems/1,各自返回数据,说明两个服务已作为独立进程运行。此刻它们还互不相识——下一课开始让它们「找到彼此」。
讲清楚要点
- 微服务按业务能力拆,不按技术层拆:拆「用户/题目/判题」是对的;拆「DAO 服务」「Service 服务」是错的——技术层拆完,每次改需求都要横跨所有服务。
- 一服务一库(理想状态):拆了进程仍共用一个 MySQL,等于只拆一半,表结构改动依然互锁。
- Spring Boot 管单个应用,Spring Cloud 管协作:Boot 解决「一个服务怎么起」,Cloud 解决「一群服务怎么找、怎么调、怎么稳」。
- Spring Cloud 不是一个大框架,而是一组 starter:注册发现、网关、配置、调用、熔断各是一个 starter,按需引入,用哪个学哪个。
- 拆分时机要克制:模块边界清晰 + 独立部署/扩容的真实诉求 + 团队规模上来,三者同时满足再拆;本站当前体量用单体是正确选择。
你可能会懵的地方
-
微服务和 SOA 是一回事吗?
SOA 是更早的「企业服务总线(ESB)」思路,靠中心化总线做协议转换;微服务去掉重总线,服务间轻量直连(HTTP/RPC),粒度更细、去中心化。面试一句「ESB 是 SOA 核心,微服务没有 ESB」即可。
-
服务拆得越细越好吗?
不是。粒度过细会导致:一次业务操作跨 N 个服务、分布式事务变多、链路排错与运维成本爆炸。经验法则:先按业务边界拆到「一个团队维护 2-5 个服务」,够用就停。
-
Spring Cloud 官方组件和 Dubbo/Nacos/K8s 什么关系?
解决同一类问题的不同方案。Dubbo 是高性能 RPC 框架;Nacos 可同时替代 Eureka 和 Config;K8s Service 自带服务发现。原理相通,学会一套再看别的都是换名字。
-
本站现在要不要拆?
不要。单机 Spring Boot + Redis + Nginx 在当前流量下性能充裕;先在代码里保持模块边界清晰(user/problem/judge 分包),等出现真实的独立扩容、独立发布诉求再拆,成本远低于过度设计。
-
微服务一定要配 Docker/K8s 吗?
不强制,本地 java -jar 也能跑全套。但进程一多(10 个以上),手工管理启动顺序、环境变量会失控,容器编排几乎是标配,最后一课会讲部署直觉。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 为简历好看把 3 人项目拆 12 个服务 | 运维成本 > 收益,分布式问题全部暴露 | 按 1-2 条业务线拆,能少则少 |
| 拆了进程仍共用一个数据库 | 表结构互锁,伪微服务 | 一服务一库,跨库数据用 API 聚合 |
| Spring Cloud 版本随手填 | 与 Boot 版本不兼容,启动报错 | 用官方版本对照表,父 pom BOM 统一管理 |
| 一个模块直接 new 另一个模块的类 | 破坏模块边界,无法独立部署 | 通过 HTTP/RPC 接口通信,共享只放 DTO |
| 在网关里写业务逻辑 | 网关膨胀成新的单体 | 网关只做路由/鉴权/限流,业务进服务 |
卡点急救
- 父工程导入后子模块依赖全红:BOM 没生效。检查子 pom 的
<parent> 是否指向父工程,先在父目录执行 mvn clean install -N 安装父 pom,再刷新子模块。
- 两个服务起一个挂一个:端口冲突。Windows 下
netstat -ano | findstr 8081 找占用进程;确认两个 yml 分别 8081/8082。
- 启动报
ClassNotFoundException 或方法不存在:Boot 与 Cloud 版本错配。Spring Boot 3.2.x 搭配 Spring Cloud 2023.0.x,不要混入 2.x 老 starter。
面试常问
-
从单体到微服务,你的拆分依据是什么?
按业务能力(DDD 的限界上下文):用户、题目、判题各自的数据模型和变更节奏不同;再叠加非功能诉求——判题是 CPU 密集型,需要独立扩容和故障隔离。忌答「拆得细就是先进」。
-
微服务带来哪些新问题?(考察有没有真做过)
分布式事务、链路排错难、数据一致性、网络不确定性(超时/重试/幂等)、运维复杂度。能说出 3 个以上并给出对应手段(熔断、trace-id、幂等键)就是加分回答。
-
Spring Cloud 和 Dubbo 怎么选?
Cloud 是全家桶(网关/配置/熔断齐),生态偏 HTTP+REST;Dubbo 高性能 RPC + 服务治理,常配 Nacos/ZooKeeper。国内现状是 Spring Cloud Alibaba(Nacos+Sentinel+Seata)与原生 Cloud 并存,原理相通,答「按团队技术栈和性能诉求选」并给出理由即可。
自测三问
- 单体的三个典型痛点是什么?分别对应微服务的哪个收益?
- Spring Boot 与 Spring Cloud 各自解决什么层次的问题?
- 「按技术层拆服务」为什么是错的?
小结一句话
微服务是把系统按业务能力拆成独立进程与独立数据库的小服务,Spring Cloud 是让这群服务互相找到、稳定协作的工具箱。
延伸