Spring Cloud从零开始:微服务入门指南
去年9月,我还在深圳某普通一本的教室里肝毕业设计,一边刷LeetCode一边投简历。拿到腾讯系某中厂offer后,入职前被组长“温柔”地提醒:“你得提前学下Spring Cloud,我们这边全量微服务了。”
我当时心里一咯噔——在学校里只用过Spring Boot写过单体CRUD应用,连Dockerfile都手抖着写,现在直接上微服务?这不是让我裸泳吗?
但没办法,打工人嘛。入职后第一天,就被拉进一个叫“凤凰计划”的重构项目群里。产品说:“我们要把原来那个巨无霸系统拆成20+个微服务,双11前上线!” 我看了一眼日历:10月12号。离双11还有不到一个月。那一刻,我真的想砸电脑。
不过好在,踩了一堆坑、熬了几个通宵、被运维和测试轮番教育之后,我总算搞明白了Spring Cloud这玩意儿到底怎么玩。今天这篇教程,就是写给和我一样——刚毕业、被赶鸭子上架、对“服务注册发现”一脸懵的新手程序员的。别怕,微服务没那么玄乎,只是配置多到让你怀疑人生罢了。
为什么非得用微服务?产品真的懂吗?
先说点扎心的:很多团队搞微服务,其实不是技术驱动,而是产品驱动(或者说“老板拍脑袋”)。我们组的产品经理老张,上周五晚上8点在群里@我:“这个功能能不能独立成一个服务?这样迭代快!”
我说:“哥,就一个发短信的接口,拆出来要搭Eureka、配Ribbon、加熔断、写健康检查……成本比收益高十倍。”
他回我:“敏捷开发懂不懂?解耦懂不懂?”
行吧,我闭嘴。
但话说回来,如果系统真的到了单体扛不住的地步(比如我们原来的订单服务动不动OOM,数据库连接池爆满),微服务确实能救命。关键在于:别为了微而微,要为了解决实际问题。
第一步:服务注册与发现 —— 别让服务“失联”
微服务最基础的问题是:A服务怎么找到B服务?以前单体时代,所有代码在一个JVM里,直接调就行。现在每个服务跑在不同机器、不同端口,总不能硬编码IP吧?(运维看到会哭)
Spring Cloud全家桶里,最经典的注册中心是 Eureka(虽然现在Nacos更火,但公司历史包袱重,还在用Eureka)。
踩坑现场 #1:服务注册了却找不到?
我第一次启动两个服务,user-service 和 order-service。order-service 要调 user-service,结果报错:
com.netflix.client.ClientException: Load balancer does not have available server for client: USER-SERVICE
当时我人傻了:明明Eureka面板里两个服务都在线啊!
后来发现:默认情况下,Eureka客户端每隔30秒才拉一次服务列表(registry fetch interval),而我的服务刚启动就立刻调用,根本来不及同步!
解决办法:在 application.yml 里调小刷新间隔:
eureka:
client:
registry-fetch-interval-seconds: 5 # 默认30,改成5秒
另外,服务名必须大写!我在 @FeignClient(name = "user-service") 里用了小写,结果死活找不到。Spring Cloud 默认把服务名转成大写去匹配 Eureka 注册的名字。血泪教训!
第二步:服务调用 —— Feign 比 RestTemplate 香多了
早期我用 RestTemplate 手动拼URL:
String url = "http://USER-SERVICE/api/users/" + id;
User user = restTemplate.getForObject(url, User.class);
结果每次改路径都要全局搜索,还容易拼错。后来学乖了,改用 OpenFeign(声明式HTTP客户端):
@FeignClient(name = "USER-SERVICE")
public interface UserClient {
@GetMapping("/api/users/{id}")
User getUserById(@PathVariable("id") Long id);
}
然后直接注入调用,像调本地方法一样:
@Autowired
private UserClient userClient;
public Order createOrder(Long userId) {
User user = userClient.getUserById(userId); // 看起来像本地调用!
// ...
}
踩坑现场 #2:Feign 默认不支持 GET 传复杂对象?
我想传一个 UserQuery 对象作为GET参数,结果Feign直接报错。查文档才知道:Feign 的 GET 请求只支持基本类型或 @RequestParam。复杂对象得用 POST,或者手动拼 query string。
后来我们统一约定:查询用 GET + 基本参数,操作用 POST/PUT。省事。
第三步:配置中心 —— 别再提交 application-prod.yml 到 Git 了!
一开始我们每个服务都有自己的 application-prod.yml,改个数据库密码要改20个文件,还得挨个重启。有一次测试环境连了生产DB,差点背锅离职。
后来上了 Spring Cloud Config(其实现在更推荐 Nacos Config,但我们还没迁移)。核心思想:配置外置,动态刷新。
配置中心结构大概这样:
config-repo/
├── user-service-prod.yml
├── order-service-prod.yml
└── application.yml # 全局默认配置
服务启动时,会自动从Git仓库拉取对应配置。更爽的是,配合 @RefreshScope,可以不用重启服务就更新配置:
@RestController
@RefreshScope // 加上这个注解
public class ConfigController {
@Value("${app.feature-flag}")
private String featureFlag;
}
然后调用 /actuator/refresh 接口就能热更新。
踩坑现场 #3:配置没生效?可能是加密问题!
公司要求数据库密码加密存储。Config Server 支持 JCEKS 加密,但我本地 JDK 没装 Unlimited Policy,解密失败,服务直接起不来。
解决方法:要么装策略文件,要么暂时关掉加密(仅测试环境!)。
第四步:熔断与降级 —— 别让一个服务崩掉全家
双11压测那天,user-service 因为数据库慢查询,响应时间飙到5秒。结果所有调它的服务(order、payment、notification)全部线程阻塞,整个系统雪崩。
这时候就需要 Hystrix(虽然官方已停更,但很多老系统还在用)或者 Sentinel(阿里开源,更现代)。
以 Hystrix 为例,给 Feign 客户端加个 fallback:
@FeignClient(name = "USER-SERVICE", fallback = UserClientFallback.class)
public interface UserClient {
@GetMapping("/api/users/{id}")
User getUserById(@PathVariable("id") Long id);
}
@Component
public class UserClientFallback implements UserClient {
@Override
public User getUserById(Long id) {
// 降级逻辑:返回空用户 or 抛异常
return new User().setId(id).setName("DEGRADED_USER");
}
}
开启 Hystrix:
feign:
hystrix:
enabled: true
踩坑现场 #4:fallback 不触发?
因为 Hystrix 默认超时时间是1秒!而我们的某些接口正常就要1.5秒。结果一到高峰期就疯狂降级,用户看到一堆“DEGRADED_USER”。
解决:调大超时时间(但别太大,否则失去意义):
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000
第五步:网关 —— 所有流量的“安检门”
微服务多了,前端不可能记住20个服务的IP和端口。于是需要一个统一入口:API Gateway。
我们用的是 Spring Cloud Gateway(不是 Zuul!Zuul 是上古时代的东西了)。
简单配置:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://USER-SERVICE # lb 表示走负载均衡
predicates:
- Path=/api/users/**
- id: order-service
uri: lb://ORDER-SERVICE
predicates:
- Path=/api/orders/**
这样,前端只要请求 gateway:8080/api/users/123,网关自动转发到 user-service。
踩坑现场 #5:跨域问题炸了!
前端本地开发(localhost:3000)调网关(localhost:8080),浏览器报 CORS 错误。
解决:在网关加全局跨域配置:
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true); // 注意:allowCredentials=true 时,allowedOrigin 不能为 *
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
生产环境那些事儿:运维视角的忠告
在学校写Demo,起5个服务觉得牛逼。但在公司,稳定性 > 一切。分享几点血泪经验:
| 问题 | 教训 |
|---|---|
| 服务启动顺序依赖 | 永远不要依赖启动顺序!用 Eureka 的 eureka.client.healthcheck.enabled=true 确保只有健康的服务才注册 |
| 日志分散难排查 | 必须上 ELK 或 Loki + Grafana,统一日志收集 |
| 链路追踪缺失 | 加上 Sleuth + Zipkin,否则线上问题靠猜 |
| 数据库连接池耗尽 | 每个微服务单独DB,连接池大小根据QPS合理设置(别照搬默认值) |
| 健康检查不完善 | 自定义 /actuator/health,检查DB、Redis、下游依赖 |
有一次,payment-service 的 Redis 连不上,但健康检查只检查了DB,导致流量进来后疯狂报错。后来我们强制要求:所有外部依赖都要纳入健康检查。
性能与架构设计:别把微服务当银弹
微服务不是万能的。我们曾把一个简单的“发送邮件”功能拆成独立服务,结果:
- 多了一次网络调用(RTT增加)
- 多了一个部署单元(运维成本+1)
- 多了一个故障点(邮件服务挂了,主流程卡住)
最后又合并回去了。所以,拆分粒度要合理。我们现在的原则是:
- 高内聚:一个服务负责一个业务域(如订单、用户、商品)
- 低耦合:通过事件(MQ)或API交互,避免直接DB共享
- 独立数据库:每个服务有自己的schema,甚至自己的MySQL实例
接口设计上,尽量用 DTO 而不是 Entity 直接暴露。曾经有人把 JPA Entity 返回给前端,结果 Hibernate LazyLoad 在序列化时炸了(著名的 LazyInitializationException)。
最后:微服务 ≠ 高级,单体 ≠ 落后
写这篇教程时,我已经入职三个月了。回头看,Spring Cloud 的学习曲线确实陡峭,但核心思想其实很简单:把分布式系统的问题,用框架帮你封装了。
如果你和我一样,是刚毕业的小白,别被“云原生”、“Service Mesh”这些词吓到。先把 Eureka、Feign、Config、Gateway 这几块搞明白,能跑通一个完整的调用链,你就已经超过80%的应届生了。
对了,上周五,产品经理又来找我:“能不能把用户服务再拆?登录和资料分开……”
我深吸一口气,微笑着说:“哥,要不我们先聊聊DDD(领域驱动设计)?”
附:快速启动模板(避坑版)
pom.xml 关键依赖(Spring Boot 2.7 + Spring Cloud 2021.0.5):
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 注意:Hystrix 已停更,新项目建议用 Resilience4j -->
application.yml 最小可用配置:
server:
port: 8081
spring:
application:
name: user-service
cloud:
config:
uri: http://config-server:8888
eureka:
client:
service-url:
defaultZone: http://eureka-server:8761/eureka/
registry-fetch-interval-seconds: 5
instance:
prefer-ip-address: true
health-check-url-path: /actuator/health
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
搞定了这套东西,至少在小组站稳脚跟了。下个月转正答辩,希望能顺利过。
(要是产品再提奇葩需求,我就祭出DDD大旗——反正他们也听不懂 😏)
共勉,打工人!

评论 0