Spring Cloud从零开始:微服务入门指南

全栈打工仔
2025-12-14 18:59
阅读 224

去年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

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝