学了Spring Boot、Redis、Docker之后,我遇到了一个灵魂拷问:项目做大了怎么办?所有功能塞在一个jar包里,改一个模块要重新部署整个项目,一个模块OOM整个应用崩溃。这一周我集中攻克了微服务架构,用Spring Cloud Alibaba全家桶把一个单体项目拆成了多个微服务,加上了注册中心、远程调用、API网关和限流熔断。这篇文章完整记录这个过程。
为什么要拆微服务?
先说我遇到的真实痛点。
之前做的图书管理项目是一个单体Spring Boot应用------用户模块、图书模块、订单模块全在一个项目里,共享一个数据库、一个端口、一个JVM进程。项目小的时候还好用,但随着功能越来越多,问题逐渐暴露:
改一行代码要重新部署整个项目。 我只改了用户模块的一个bug,但要把整个应用(包括图书、订单模块)全部重新打包、重新部署。每次发布都要全量回归测试。
一个模块的问题会拖垮整个应用。 图书模块有个慢查询把连接池占满了,整个JVM的线程都在等,用户模块也跟着不可用。明明是两个不相干的功能,却互相牵连。
没办法按需扩容。 图书查询的流量最大,我想多开几个图书服务的副本?不行,只能把整个大应用多开几份,连带用户模块也跟着多开了,浪费资源。
这三个问题让我决定:是时候学微服务了。
一、单体 vs 微服务:一张图说清楚
单体架构就是所有模块打在一个jar包里,跑在一个JVM进程里:
┌─────────────────────────────────┐
│ bookstore.jar │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │用户 │ │图书 │ │订单 │ │
│ │模块 │ │模块 │ │模块 │ │
│ └─────┘ └─────┘ └─────┘ │
│ 共享一个数据库和端口 │
└─────────────────────────────────┘
微服务架构是每个模块拆成独立的服务,各自有自己的端口、数据库、进程:
┌────────┐ ┌────────┐ ┌────────┐
│ user- │ │ book- │ │ order- │
│service │ │service │ │service │
│ :8081 │ │ :8082 │ │ :8083 │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
各自独立的数据库和部署
微服务的好处很直接:独立部署(改用户模块只重新部署用户服务)、独立扩缩容(图书服务流量大就多开几个副本)、故障隔离(订单服务挂了不影响用户和图书)。
但代价也很明显:以前一个方法调用搞定的事,现在要发HTTP请求;以前一个数据库事务搞定的事,现在要做分布式事务;以前一个项目就完了,现在要管好几个服务的部署和监控。
我的结论:小项目别硬上微服务。 当你的项目功能模块超过5个、团队超过5个人、需要独立扩缩容时,微服务才有意义。学习的时候先用小项目练手,理解原理最重要。
二、Spring Cloud Alibaba 全家桶一览
Java生态里做微服务最主流的方案是Spring Cloud。国内企业用得最多的是阿里开源的Spring Cloud Alibaba。这周我学的四个核心组件:
| 组件 | 解决的问题 | 比喻 |
|---|---|---|
| Nacos | 服务在哪?怎么找到它? | 公司通讯录 |
| OpenFeign | 服务之间怎么调用? | 内部电话系统 |
| Gateway | 客户端怎么统一访问? | 公司前台 |
| Sentinel | 流量太大怎么保护? | 门禁限流系统 |
下面逐个分享我的学习过程和踩坑经验。
三、Nacos:服务的"通讯录"
3.1 服务注册发现到底在解决什么
两个微服务互相调用时,book-service怎么知道user-service的IP和端口?
笨办法是写死地址。但user-service如果扩缩容了(IP变了或多了几个副本),你得改配置重启book-service。
Nacos的做法是引入一个"中间人"。所有服务启动时向Nacos注册自己的地址,需要调用别的服务时问Nacos要地址。就像公司通讯录------新员工入职时登记分机号,你要找人就查通讯录,不用记住每个人的号码。
完整流程是四步:注册(服务启动时向Nacos报告地址)→ 心跳续约(每5秒发心跳表示存活)→ 服务发现(调用方查询目标地址列表)→ 下线注销(正常关闭时通知Nacos移除)。
3.2 安装和启动
Nacos是一个独立运行的服务端程序。去GitHub下载nacos-server的zip包,解压后执行:
bash
startup.cmd -m standalone
浏览器打开 http://localhost:8848/nacos,用nacos/nacos登录就能看到控制台。
3.3 让服务注册到Nacos
在Spring Boot项目里只需要三步:
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: user-service # 服务名(注册到Nacos上的标识)
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos在哪
启动服务后,Nacos控制台的服务列表里就能看到它了。就这么简单。
四、服务间调用:从RestTemplate到OpenFeign
4.1 RestTemplate(能用但啰嗦)
第一个想到的方案是用RestTemplate发HTTP请求:
java
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
// 调用时:
String url = "http://user-service/users/" + userId;
UserDTO user = restTemplate.getForObject(url, UserDTO.class);
@LoadBalanced是关键注解------它让RestTemplate能把URL里的服务名user-service通过Nacos解析成实际IP:端口,并做负载均衡选择一个实例。
但这个写法有几个问题:URL要手动拼字符串,返回类型要手动指定(编译期不检查),每个调用点都要写重复代码。
4.2 OpenFeign(优雅太多了)
OpenFeign把远程调用变成了一个Java接口:
java
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/users/{id}")
UserDTO getUserById(@PathVariable("id") Long id);
}
// 使用时直接注入:
@Autowired
private UserClient userClient;
UserDTO user = userClient.getUserById(1L);
// 看起来像调本地方法,实际上发了一次HTTP请求
从两行变成了半行。而且URL、返回类型、参数绑定全在接口定义里,编译期就能检查类型是否正确。
底层原理是JDK动态代理。 Spring启动时@EnableFeignClients扫描所有@FeignClient接口,为每个接口创建动态代理对象。你调用方法时,代理拦截调用:从注解解析URL → Nacos查地址 → 负载均衡选实例 → 发HTTP请求 → JSON反序列化返回。面试很爱考这个原理。
4.3 超时和日志(必须配)
OpenFeign默认超时很短(1秒连接+1秒读取),生产环境必须调整:
yaml
spring:
cloud:
openfeign:
client:
config:
default:
connect-timeout: 3000
read-timeout: 5000
logger-level: FULL # 开发阶段开FULL,生产用NONE
不配超时的后果:下游服务慢了,上游的线程一直阻塞等响应,线程池耗尽,上游也跟着崩------级联故障。这是我踩过的一个真实的坑。
4.4 Token透传
微服务间调用还有一个容易忽略的问题:用户的JWT Token不会自动跟着Feign请求走。你发了一个全新的HTTP请求,默认不带原始请求的请求头。
解决方案是写一个RequestInterceptor,每次Feign发请求前从当前线程的HttpServletRequest中取出Authorization头加进去:
java
@Bean
public RequestInterceptor tokenRelayInterceptor() {
return template -> {
ServletRequestAttributes attrs =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
String auth = attrs.getRequest().getHeader("Authorization");
if (auth != null) {
template.header("Authorization", auth);
}
}
};
}
这样Token就能在整条调用链路上自动传递,每个服务都能做鉴权。
五、Gateway:微服务的"统一前台"
5.1 为什么需要网关
没有网关时,客户端要知道每个服务的地址和端口,每个服务都要自己做JWT校验和跨域处理。有了网关,所有请求先经过Gateway,由Gateway做路由分发、统一鉴权、统一跨域。后端服务不对外暴露端口,只有Gateway的端口对外开放。
5.2 踩坑:不能引spring-boot-starter-web
这是我第一次用Gateway时踩的最痛的坑。Gateway基于Spring WebFlux(响应式),底层用Netty。而spring-boot-starter-web基于Servlet/Tomcat。两者不能共存,引了web依赖启动直接报错。
正确做法:gateway-service的pom.xml里只引spring-cloud-starter-gateway、nacos-discovery和loadbalancer,不要引web。
5.3 路由配置
Gateway的核心就是路由规则------什么请求转发到哪个服务:
yaml
spring:
cloud:
gateway:
routes:
- id: user-route
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- StripPrefix=2
- id: book-route
uri: lb://book-service
predicates:
- Path=/api/book/**
filters:
- StripPrefix=2
lb://表示通过Nacos负载均衡找服务。Path=/api/user/**是匹配条件。StripPrefix=2在转发时去掉前两层路径(/api/user),因为后端Controller的路径是/users/{id}而不是/api/user/users/{id}。
配好之后,客户端只需要知道Gateway的端口:
bash
# 所有请求都走Gateway
curl http://localhost:9000/api/user/users/1 # → 转发到user-service
curl http://localhost:9000/api/book/books/1 # → 转发到book-service
5.4 全局过滤器做JWT鉴权
Gateway最有价值的用途之一是把JWT校验集中在一处。写一个GlobalFilter,所有请求进来先校验Token,无效直接返回401,有效就把用户ID放进请求头转发给后端:
java
@Component
public class AuthFilter implements GlobalFilter, Ordered {
private static final List<String> WHITE_LIST = List.of(
"/api/user/users/login", "/api/user/users/register"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// 白名单直接放行
if (WHITE_LIST.stream().anyMatch(path::startsWith)) {
return chain.filter(exchange);
}
// 取Token并校验
String auth = exchange.getRequest().getHeaders().getFirst("Authorization");
if (auth == null || !auth.startsWith("Bearer ")) {
return unauthorized(exchange);
}
String token = auth.substring(7);
String userId = parseToken(token); // JWT解析
// 把用户ID放进请求头,后端服务直接用
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.header("X-User-Id", userId)
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
@Override
public int getOrder() { return -1; }
}
这样后端的每个Controller直接从请求头拿用户ID,不用再各自解析JWT了。改鉴权逻辑也只需要改Gateway一处。
六、Sentinel:微服务的"保险丝"
6.1 限流的必要性
每个服务和数据库都有处理能力上限。突发流量超过上限时服务直接被压垮,然后级联故障扩散到整个系统。限流就是保护服务在能力范围内工作------超过阈值的请求排队或直接拒绝。
比喻就是网红餐厅门口的叫号机:里面座位满了就排队,不让所有人同时涌进去。
6.2 安装Sentinel
引入依赖、配置Dashboard地址、启动Dashboard(一个独立的jar包):
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
yaml
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
bash
java -jar sentinel-dashboard-1.8.8.jar
Dashboard提供可视化界面,可以实时查看每个接口的QPS、配限流规则、看熔断状态。
6.3 限流规则
在Dashboard上给/books/{id}接口配QPS阈值为10------每秒最多10个请求,超过的直接拒绝返回429。
三种限流算法:快速失败(超了直接拒绝,最常用)、Warm Up(冷启动,阈值慢慢升高)、排队等待(匀速放行,流量整形)。底层用的是滑动窗口统计QPS和令牌桶做限流。面试爱考这两者的区别。
6.4 熔断降级
限流是保护自身接口不被太多请求压垮,熔断是保护上游调下游时不被拖死。
当book-service调用user-service,user-service响应越来越慢时,Sentinel检测到异常率超过阈值会自动"熔断"------不再调用user-service,直接走降级逻辑返回默认值。等user-service恢复了,自动关闭熔断恢复正常。
熔断器有三种状态:CLOSED(正常调用)→ 异常率超标 → OPEN(不再调用,走降级)→ 等一会 → HALF_OPEN(试探一个请求)→ 成功则回CLOSED,失败则回OPEN。
6.5 Feign + Sentinel 集成
最优雅的方式是让OpenFeign和Sentinel联动。Feign调用失败时自动走fallback,不需要在Controller里写try-catch:
java
@FeignClient(name = "user-service", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/users/{id}")
UserDTO getUserById(@PathVariable("id") Long id);
}
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
return id -> {
UserDTO user = new UserDTO();
user.setId(id);
user.setName("服务暂不可用");
return user;
};
}
}
用fallbackFactory比fallback好------能拿到具体的异常信息,可以针对超时、连接拒绝等不同原因返回不同的降级结果。
七、整体串联:四个组件如何协作
把这周学的东西串起来看,一个完整的请求链路是这样的:
客户端
│
│ GET http://gateway:9000/api/book/books/1
│ Authorization: Bearer xxx
↓
Gateway(统一入口)
├── AuthFilter:校验JWT Token → 通过 ✅
├── AccessLogFilter:记录请求日志
└── 路由匹配:/api/book/** → book-service
│ StripPrefix=2,去掉 /api/book
↓
book-service (:8082)
├── Sentinel检查:QPS是否超阈值 → 未超 ✅
├── 查询图书信息
├── OpenFeign调用 user-service 获取作者信息
│ ├── 从Nacos查user-service地址
│ ├── 负载均衡选一个实例
│ ├── Token透传(RequestInterceptor)
│ └── Sentinel熔断保护(失败走fallback)
└── 组装完整响应返回
↓
Gateway → 客户端
四个组件各司其职:Gateway管入口和路由,Nacos管服务地址,OpenFeign管服务间调用,Sentinel管流量保护。缺了任何一个,微服务架构都不完整。
八、版本对应关系(踩坑预警)
这是我花了最多时间排查的问题:Spring Boot、Spring Cloud、Spring Cloud Alibaba三者的版本必须对应。 版本不匹配会出现各种莫名其妙的兼容问题------类找不到、方法不存在、配置不生效。
我用的组合是:Spring Boot 3.2.5 + Spring Cloud 2023.0.1 + Spring Cloud Alibaba 2023.0.1.0。建议大家也用这个组合,是目前最稳定的。
版本对应关系可以查Spring Cloud Alibaba官方文档。强烈建议用父工程的dependencyManagement统一管理版本,子模块不要各自写version。
九、个人感悟
这一周最大的收获是理解了微服务架构的核心思想:用架构复杂度换开发和部署的灵活性。
之前我觉得微服务就是"把项目拆小",学了之后才发现拆分只是第一步。拆完之后服务间怎么通信、怎么找到对方、怎么统一入口、怎么保护流量------每一步都有对应的技术方案。Spring Cloud Alibaba把这四件事都做成了"开箱即用"的starter,降低了入门门槛,但理解背后的原理同样重要。
面试问微服务不是为了考你会不会配yml,而是看你理解不理解为什么要这么做。比如面试官问"为什么需要服务注册发现",你不能只说"因为要用Nacos",而应该说"因为微服务的地址是动态的,扩缩容和重启都会导致地址变化,需要一个中心化的服务来管理所有实例的地址"。
还有一个感触:微服务不是银弹。 它解决了单体架构的部署和扩展问题,但引入了分布式通信、数据一致性、运维复杂度等新问题。选型时要根据团队规模和业务需求来决定,不要为了微服务而微服务。
下周开始进入中间件的学习(RabbitMQ/Kafka消息队列、Elasticsearch搜索引擎),会继续记录。如果这篇文章对你有帮助,点个赞再走吧。