📚 本系列系统梳理了 Java 开发的详细知识点,从基础语法到工程实践层层递进,内容详实成体系,建议先收藏再慢慢阅读,方便日后随时回顾查阅。
前言
前面 27 篇基本都是"一个应用、一个数据库"的视角------写 Controller、Service、Mapper,连上 MySQL/Redis 就能跑。但当系统越来越大(比如一个完整的广告投放平台,要做投放管理、计费、报表、用户中心),继续把所有功能塞进一个项目,会越来越难维护。这篇文章站在初学者的角度,梳理"微服务"到底是什么、为什么要拆、拆了之后会冒出哪些新问题,以及 Spring Cloud 生态里对应的解决方案------目标是建立一个完整的概念地图,不深挖某一个组件的所有细节。
1. 为什么需要微服务
1.1 单体应用:一开始很香
把"广告投放平台"的所有功能------用户管理、广告投放、计费结算、数据报表------都写在一个项目 里,打包成一个 jar,部署成一个进程。这就是单体架构(Monolith)。
| 优点 | 缺点 |
|---|---|
| 开发简单:一个项目,一次启动,本地调试方便 | 代码越大越难维护:改一个小功能,可能要重新理解一大片代码 |
| 部署简单:一个 jar,一台服务器 | 牵一发动全身:改了报表模块的代码,整个应用都要重新打包、重新部署 |
| 调用简单:模块间是 Java 方法调用,没有网络开销 | 扩展不灵活:报表模块需要更多 CPU,但只能把整个应用一起扩容 |
| 技术栈绑定:整个项目只能用一种语言、一套框架 |
团队小、业务简单的时候,单体应用是最优选择------不要为了"赶时髦"而过早拆微服务,拆分本身是有代价的(后面会讲)。但当团队变大、业务模块变多,单体的"缺点"那一列会越来越明显:几十个人同时改一个项目,光是合并代码冲突就能耗费大量时间。
1.2 微服务:按业务拆成多个独立的应用
微服务架构(Microservices) 把单体应用按业务拆成多个独立部署、独立运行的小应用,每个小应用:
- 只负责一块业务(用户服务、投放服务、计费服务、报表服务......)
- 有自己的代码仓库、可以独立开发和部署
- 一般有自己独立的数据库(不共享表)
- 服务之间通过网络请求(HTTP/RPC)通信,不再是 Java 方法调用


1.3 微服务不是免费的午餐
拆分之后,每个模块内部 确实变简单了------投放服务只用关心投放逻辑。但模块之间的复杂度从"方法调用"变成了"网络调用",这就引入了一整套单体应用里根本不存在的新问题:
| 单体应用里 | 微服务里 |
|---|---|
| 模块 A 调用模块 B:一次本地方法调用,几乎不耗时,不会失败 | 服务 A 调用服务 B:要先知道 B 在哪台机器上、走网络请求,可能超时、可能失败 |
| 一个数据库事务搞定多表更新 | 跨服务的数据一致性,本地事务管不到别的服务 |
| 改配置改完重启一下就行 | 几十个服务实例,配置怎么统一改、统一生效 |
| 一个日志文件,出错了直接搜关键字 | 一个请求经过 5 个服务,日志分散在 5 个地方,怎么串起来排查 |
接下来每一节,都是在回答"拆分之后冒出来的这个新问题,业界是怎么解决的"。
2. 微服务要解决的核心问题:总览
把上面的问题对应到具体技术方案,先建立一张"地图",后面每一节展开讲一个:
| 问题 | 一句话描述 | 常见方案 |
|---|---|---|
| 服务怎么找到对方 | 投放服务想调用计费服务,但计费服务部署在哪台机器、哪个端口? | 注册中心(Eureka / Nacos) |
| 服务之间怎么调用 | 找到了地址,怎么发起调用、怎么少写重复代码? | OpenFeign / RestTemplate |
| 请求量大了怎么分摊 | 计费服务部署了 3 个实例,请求该打给哪一个? | 负载均衡(LoadBalancer) |
| 外部请求怎么进来 | 手机 App 不会直接调用内部几十个服务,应该打到哪? | API 网关(Spring Cloud Gateway) |
| 配置怎么统一管理 | 几十个服务的数据库地址、开关配置,难道一个个改? | 配置中心(Nacos Config) |
| 某个服务挂了怎么办 | 计费服务卡死,会不会拖垮调用它的所有服务? | 熔断与降级(Sentinel) |
| 跨服务的数据一致性 | "扣费成功但投放记录创建失败",怎么保证两边一致? | 分布式事务(Seata) |
| 出问题了怎么排查 | 一个慢请求,经过了 5 个服务,慢在哪一环? | 链路追踪(Sleuth + Zipkin) |
| 怎么部署这么多服务 | 几十个服务,手动一个个启动、升级,根本管不过来 | 容器化(Docker + Kubernetes) |
下面按这张表的顺序,逐个用最简单的例子讲清楚"是什么、为什么需要、长什么样"。
3. 服务注册与发现
3.1 为什么需要
假设投放服务要调用计费服务的接口 POST /billing/charge。在单体应用里这就是一次方法调用;现在计费服务是独立部署的,投放服务必须知道:计费服务现在跑在哪台机器、哪个端口。
最朴素的办法是把地址写死在配置文件里:
yaml
billing-service:
url: http://192.168.1.100:8081
问题来了:
- 计费服务扩容到 3 个实例,写一个地址够吗?
- 某个实例重启后 IP 变了,配置文件要不要手动改?
- 服务越来越多,每个服务都要维护一份"别人的地址表"
3.2 注册中心:服务的"电话簿"
注册中心就是一个所有服务都能访问的"电话簿":
- 服务注册:计费服务启动时,把自己的"名字 + IP + 端口"报告给注册中心
- 心跳续约:计费服务定期向注册中心发心跳,证明自己还活着;注册中心如果一段时间没收到心跳,就把这个实例摘除
- 服务发现:投放服务要调用计费服务时,先问注册中心"计费服务现在有哪些可用实例",拿到一个地址列表

常见注册中心:Netflix Eureka(Spring Cloud 早期默认)、阿里 Nacos(同时支持注册中心 + 配置中心,国内更主流)。
3.3 最简示例(Spring Cloud Alibaba + Nacos)
xml
<!-- pom.xml:让服务接入 Nacos 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
yaml
# application.yml
spring:
application:
name: billing-service # 注册到 Nacos 的服务名
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
服务启动后,就会自动注册到 Nacos------不需要写任何代码,靠的是 Spring Boot 的自动装配机制(参考 17 节 Spring Boot)。
4. 服务间调用:OpenFeign
4.1 用 RestTemplate 调用:能跑,但很啰嗦
22 节介绍过 RestTemplate(参考 22 节 HTTP 客户端),调用别的服务时可以这样写:
java
String url = "http://billing-service/billing/charge"; // billing-service 是注册中心里的服务名
ChargeResult result = restTemplate.postForObject(url, request, ChargeResult.class);
每次调用都要手写 URL、手动拼参数、手动处理返回值------服务调用一多,代码里到处都是这种字符串拼接。
4.2 OpenFeign:把远程调用"伪装"成本地方法
OpenFeign 的思路:定义一个 Java 接口,加上注解描述"调用哪个服务的哪个接口",剩下的事情(拼 URL、发 HTTP 请求、解析响应)框架自动完成------业务代码里调用这个接口,就像调用本地的 Service 一样。
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
java
// 1. 定义接口:声明"我要调用 billing-service 的 /billing/charge"
@FeignClient(name = "billing-service")
public interface BillingClient {
@PostMapping("/billing/charge")
ChargeResult charge(@RequestBody ChargeRequest request);
}
java
// 2. 像调用普通 Service 一样使用
@Service
@RequiredArgsConstructor
public class AdService {
private final BillingClient billingClient;
public void launchAd(Long adId) {
ChargeResult result = billingClient.charge(new ChargeRequest(adId, 100));
// ... 后续逻辑
}
}
@FeignClient(name = "billing-service") 里的 billing-service 就是 3.3 节里计费服务注册到 Nacos 时用的名字------OpenFeign 会自动去注册中心查这个名字对应的地址列表,调用方完全不用关心具体 IP。
5. 负载均衡
5.1 为什么需要
计费服务为了扛住更大流量,部署了 3 个实例。投放服务通过注册中心拿到的是一个地址列表(3 个地址),每次调用该选哪一个?
5.2 负载均衡器:在多个实例间分配请求
负载均衡(Load Balancing) 就是按某种策略,把请求分摊到多个服务实例上:
| 策略 | 说明 |
|---|---|
| 轮询(Round Robin) | 依次轮流分配给每个实例,最简单也最常用 |
| 随机(Random) | 随机选一个实例 |
| 加权(Weighted) | 性能好的机器配置更高权重,分配更多请求 |
Spring Cloud 里,OpenFeign(4 节)和 RestTemplate 都可以直接集成 spring-cloud-starter-loadbalancer------你在 4.2 节写的 BillingClient 调用,背后实际上已经经过了负载均衡器,业务代码完全无感知。这也是注册中心 + 负载均衡组合的核心价值:调用方只写"服务名","该打给哪个实例"完全交给框架处理。

6. API 网关
6.1 为什么需要
现在系统内部有用户服务、投放服务、计费服务、报表服务......前端(网页/App)要调用这些接口时,难道要分别记住每个服务的地址吗?而且像"登录校验""限流""统一加密"这些逻辑,难道每个服务都要重复写一遍?
6.2 网关:所有外部请求的统一入口
API 网关 是部署在最外层的一个独立服务,所有外部请求先经过网关,再由网关转发给内部对应的服务:

网关的核心能力:
| 能力 | 说明 |
|---|---|
| 路由 | 根据请求路径,把 /api/users/** 转发给用户服务,/api/ads/** 转发给投放服务 |
| 统一鉴权 | 在网关层校验 Token,校验失败直接拒绝,内部服务不用每个都写一遍登录校验 |
| 限流 | 控制每秒最多放行多少请求,保护后端服务 |
| 日志/监控 | 统一记录每个请求的耗时、状态码 |
Spring Cloud Gateway 的路由配置示例:
yaml
spring:
cloud:
gateway:
routes:
- id: ad-service-route
uri: lb://ad-service # lb:// 表示走负载均衡,ad-service 是注册中心里的服务名
predicates:
- Path=/api/ads/** # 匹配这个路径前缀的请求
- id: billing-service-route
uri: lb://billing-service
predicates:
- Path=/api/billing/**
7. 配置中心
7.1 为什么需要
每个服务都有 application.yml------数据库地址、Redis 地址、第三方接口的开关。当服务数量变多,或者同一个服务部署了几十个实例:
- 改一个配置(比如把某个开关从
false改成true),要逐个服务、逐个实例去改配置文件再重启吗? - 测试环境和生产环境的配置经常不一样,怎么管理?
7.2 配置中心:配置集中存储,动态推送
配置中心 把配置文件统一存放在一个中心服务里,各个服务启动时从配置中心拉取自己的配置;配置中心修改配置后,还能实时推送给正在运行的服务,不用重启。
yaml
# bootstrap.yml:告诉服务去 Nacos 拉取配置
spring:
application:
name: billing-service
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yaml # 对应 Nacos 上的 billing-service.yaml
配合 @RefreshScope 注解,被它标注的 Bean 在配置中心的值变化时会自动刷新,不需要重启服务:
java
@RestController
@RefreshScope
public class FeatureController {
@Value("${feature.new-billing-enabled:false}")
private boolean newBillingEnabled;
}
Nacos 同时承担"注册中心"(3 节)和"配置中心"两个角色,是它在国内 Spring Cloud Alibaba 生态里很受欢迎的原因之一。
8. 服务容错:熔断、降级、限流
8.1 为什么需要:一个服务挂了,不能拖垮所有人
假设报表服务调用计费服务查询账单数据,但计费服务因为数据库慢查询卡住了,每个请求都要等 30 秒才超时。这时:
- 报表服务的所有请求都卡在等计费服务的调用上
- 报表服务的线程池被这些"卡住的请求"占满
- 报表服务自己也变得不可用了------故障从一个服务扩散到了调用它的所有服务,这就是"雪崩"
8.2 三个关键手段
| 手段 | 含义 | 类比 |
|---|---|---|
| 限流 | 限制单位时间内允许通过的请求数量,超出的直接拒绝 | 景区限制每天入园人数 |
| 熔断 | 监控调用失败率,失败率过高时暂时停止调用这个服务一段时间,直接返回失败/默认值,不再傻等 | 家里的保险丝,电流过大就跳闸 |
| 降级 | 调用失败/熔断时,返回一个"兜底"结果(比如缓存数据、默认值),而不是直接报错给用户 | 高速路口堵车时,导航改走备用路线 |
8.3 熔断器的三种状态

- 关闭:正常状态,请求正常发给计费服务
- 打开 :失败率超过阈值后,熔断器"跳闸",后续请求不再真正调用计费服务,直接走降级逻辑(比如返回"账单暂不可用,请稍后重试")
- 半开:经过一段冷却时间后,放一小部分请求"试探"计费服务是否恢复------如果成功就回到"关闭"状态,否则继续保持"打开"
常见组件:Sentinel(阿里,支持限流+熔断+降级,有可视化控制台)、Resilience4j(轻量级库)。Netflix Hystrix 是早期的代表,但已停止维护。
java
// Resilience4j 风格的熔断示例(伪代码示意,实际配置以注解/配置文件为主)
@CircuitBreaker(name = "billingService", fallbackMethod = "fallbackGetBill")
public Bill getBill(Long adId) {
return billingClient.getBill(adId);
}
// 熔断或异常时的降级方法,方法签名要和原方法一致(多一个 Throwable 参数)
public Bill fallbackGetBill(Long adId, Throwable t) {
return Bill.unavailable(); // 返回一个表示"暂不可用"的默认对象
}
9. 分布式事务
9.1 为什么需要
广告"投放"操作需要同时做两件事:
- 计费服务:给广告主账户扣费
- 投放服务:创建一条投放记录
在单体应用里,这两步在一个 @Transactional 方法里,要么都成功要么都回滚(参考 MyBatis/Spring 事务管理)。但现在这是两个服务、两个独立数据库------本地事务管不到另一个服务的数据库。如果"扣费成功"但"创建投放记录"失败了,钱扣了但广告没投出去,数据就不一致了。
9.2 几种常见方案(了解即可,不深入)
| 方案 | 思路 | 特点 |
|---|---|---|
| 2PC(两阶段提交) | 协调者先问所有参与者"能不能提交",都说能才真正提交 | 强一致,但所有参与者要互相等待,性能差,很少在微服务里直接用 |
| TCC(Try-Confirm-Cancel) | 每个操作拆成"预留资源(Try)"、"确认执行(Confirm)"、"取消释放(Cancel)"三步 | 性能较好,但每个业务都要写三套接口,开发成本高 |
| Saga | 把一个大事务拆成一串本地事务,每一步失败时执行对应的"补偿操作"(比如"扣费"对应的补偿是"退款") | 不要求强一致,适合电商/广告这类"最终一致"场景 |
| 本地消息表 / 消息队列 | 先在本地数据库记一条"待发送"消息,本地事务提交后再异步通知其他服务,失败可重试 | 实现简单,对最终一致性场景很常用 |
实际项目中,这部分常用阿里的 Seata 框架封装了上述模式(尤其是 AT 模式,类似自动化的 2PC),业务代码改动很小。初学阶段记住一个结论 :分布式事务追求的通常是最终一致性(过程中可能短暂不一致,但最终会达成一致),而不是单体应用里的强一致性。
10. 链路追踪与日志聚合
10.1 为什么需要
一个"创建广告投放"的请求,可能依次经过:网关 → 投放服务 → 计费服务 → 用户服务(查询广告主余额)→ 报表服务(记录一条投放日志)。如果这个请求总共耗时 2 秒,慢在哪一步 ?如果报错了,是哪个服务报的错?
每个服务的日志都是独立文件,单看一个服务的日志,看不到完整的调用链路。
10.2 链路追踪:给每个请求一个"身份证号"
链路追踪 的核心思路很简单:请求进入网关时,生成一个全局唯一的 TraceId;这个 TraceId 会随着请求(通过 HTTP Header)一路传递给后续调用的每个服务;每个服务打日志时,都把这个 TraceId 一起打印出来。
TraceId: abc123
网关 [abc123] 收到请求 /api/ads/launch
投放服务 [abc123] 处理投放请求,调用计费服务
计费服务 [abc123] 扣费成功,耗时 800ms ← 原来慢在这里!
投放服务 [abc123] 创建投放记录成功
排查问题时,只要拿着这个 TraceId 去日志系统里搜索,就能把分散在 4 个服务里的日志串成一条完整的调用链,并看到每一步的耗时。
常见组合:
- Spring Cloud Sleuth (生成和传递 TraceId)+ Zipkin(可视化展示调用链路和耗时)
- SkyWalking:国产 APM 工具,功能更全(链路追踪 + 性能监控 + 告警),接入时几乎不用改代码
- ELK(Elasticsearch + Logstash + Kibana):把所有服务的日志统一收集、索引,支持按 TraceId 搜索
11. 容器化与部署简介
11.1 为什么需要
微服务拆分之后,服务数量从 1 个变成几十个。每个服务可能用了不同版本的 JDK、不同的依赖库版本------在物理机/虚拟机上手动部署、手动处理"环境不一致"的问题,几十个服务根本管不过来。
11.2 Docker:把"运行环境"打包进去
Docker 把应用代码、运行时依赖(JDK 版本、系统库)、配置文件,一起打包成一个镜像(Image)。这个镜像在任何装了 Docker 的机器上,运行出来的**容器(Container)**行为都完全一致------彻底解决"在我电脑上能跑"的问题。
dockerfile
# Dockerfile:描述如何构建 billing-service 的镜像
FROM eclipse-temurin:17-jre
COPY target/billing-service.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
bash
# 构建镜像
docker build -t billing-service:1.0 .
# 运行容器
docker run -d -p 8081:8081 billing-service:1.0
11.3 Kubernetes:管理大量容器
几十个服务、每个服务还要部署多个实例,光靠 docker run 一个个启动也不现实。Kubernetes(K8s) 是容器编排平台,负责:
| 能力 | 说明 |
|---|---|
| 自动调度 | 把容器分配到集群里资源充足的机器上运行 |
| 自动扩缩容 | 根据 CPU/内存使用率,自动增加或减少服务实例数量 |
| 自动恢复 | 容器挂了自动重启,机器挂了自动迁移到其他机器 |
| 服务发现 | K8s 内置了类似 3 节"注册中心"的能力 |
| 滚动更新 | 发布新版本时逐个替换旧实例,过程中服务不中断 |
初学阶段不需要精通 K8s 的所有概念,知道"它是用来管理大量容器的"就足够建立起完整的认知地图了------后续可以专门写一篇深入。
12. 小结
| 主题 | 核心问题 | 关键方案 |
|---|---|---|
| 服务注册与发现 | 服务怎么找到对方 | Nacos / Eureka:注册 + 心跳 + 发现 |
| 服务调用 | 调用代码怎么写得简洁 | OpenFeign:把远程调用伪装成接口方法 |
| 负载均衡 | 多个实例怎么分摊请求 | LoadBalancer:轮询/随机/加权,对调用方透明 |
| API 网关 | 外部请求统一入口在哪 | Spring Cloud Gateway:路由 + 鉴权 + 限流 |
| 配置中心 | 配置怎么集中管理、动态生效 | Nacos Config + @RefreshScope |
| 服务容错 | 一个服务挂了怎么不拖垮全部 | 限流/熔断/降级,Sentinel / Resilience4j |
| 分布式事务 | 跨服务数据一致性 | 追求最终一致性:Saga / 消息队列 / Seata |
| 链路追踪 | 一个慢请求慢在哪一步 | TraceId 贯穿全链路,Sleuth + Zipkin / SkyWalking |
| 容器化部署 | 几十个服务怎么管 | Docker 打包 + Kubernetes 编排 |
给初学者的建议 :不要一上来就想着把所有组件都用上------先理解"单体的痛点 → 拆分之后冒出的新问题 → 每个新问题对应的方案"这条逻辑链,再结合实际项目规模决定要不要引入某个组件。Mini-SSP 这类中小型项目,初期甚至可以先不拆微服务,把"按业务模块划分代码包"做好(参考 16 节 Spring IoC/AOP 的分层思想),等真的遇到"一个团队改一个项目互相阻塞"的问题时再考虑拆分。
🎯 如果这篇文章对你有帮助,别忘了点赞、收藏、关注三连!关注我,让你在 Java 学习的道路上不迷路,持续为你带来成体系的 Java 干货~