Spring Cloud从零开始:一个微服务实战者的成长笔记

郑雨泽
2025-06-20 02:23
阅读 563

引子:为什么我决定用Spring Cloud重构项目?

引子:为什么我决定用Spring Cloud重构项目?

那是一个不算太久远的春天,我们团队接手了一个中型电商平台的升级项目。老系统是典型的单体架构,部署在一台ECS服务器上,数据库是MySQL,前端用Vue 2写的SPA页面,后端基于Spring Boot开发。

当时的问题很突出:

  • 每次更新一个模块都要全量发布,风险极大;
  • 随着用户增长,订单服务和库存服务经常成为瓶颈;
  • 新功能迭代周期越来越长,代码库臃肿不堪;
  • 不同业务之间高度耦合,维护成本很高。

老板拍板要做微服务化改造,技术选型的时候,我和几个骨干工程师开了几天会,最终选择了Spring Cloud Alibaba + Nacos + Gateway + Feign + Sentinel的技术栈。说实话,这对我来说也是一次全新的尝试,但事实证明,这次选择非常明智。

这篇文章,我会以第一人称来分享我的真实经验和踩过的坑,希望对正在考虑或者刚开始微服务之路的同学有所帮助。


初探微服务:我们的第一个Spring Cloud项目是如何诞生的?

初探微服务:我们的第一个Spring Cloud项目是如何诞生的?

一开始我们定下了几个目标:

  1. 逐步拆分、平滑过渡,不能为了拆而拆;
  2. 保持原有功能的同时进行改造
  3. 确保性能不降,甚至要有所提升
  4. 运维自动化程度提高,减少人为干预错误

于是,我们在原Spring Boot的基础上,引入了Spring Cloud Starter Alibaba,并搭建了基础的注册中心——Nacos Server。

第一步:搭建注册中心(Nacos)

我们选择将Nacos作为服务注册与配置中心。最初只是跑在本地的一个Docker容器里:

docker run -d --name nacos-server -p 8848:8848 nacos/nacos-server:latest

然后在各个Spring Boot项目的application.yml中配置服务注册:

spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

启动后就能看到服务自动注册到了Nacos控制台。那一刻,心里莫名有种“我终于开始微服务了”的兴奋感。


真正挑战来了:服务拆分和通信难题

在实际开发中,我们把原来的大系统分成了以下几个核心服务:

  • 用户服务(user-service)
  • 商品服务(product-service)
  • 订单服务(order-service)
  • 库存服务(inventory-service)
  • 支付服务(payment-service)

每个服务单独打成jar包,通过不同的端口运行。接下来遇到的第一个大问题就是服务间如何通信?

最开始我们直接用RestTemplate调用:

String url = "http://localhost:9001/product/{id}";
Product product = restTemplate.getForObject(url, Product.class, productId);

但在分布式环境下,这种写死的方式显然不可取,而且不具备容错能力。

于是我们引入了Feign Client:

@FeignClient(name = "product-service")
public interface ProductServiceClient {
    @GetMapping("/products/{id}")
    Product getProductById(@PathVariable("id") Long id);
}

这样一来,Feign会自动通过Ribbon做客户端负载均衡,结合Nacos的服务发现机制,就可以做到动态调用目标服务。

不过,别高兴得太早!


惊天一问:服务雪崩怎么办?

有一次测试环境压测时,我们模拟商品服务挂掉,结果整个订单流程都瘫痪了。因为订单服务依赖商品服务查询价格,而库存服务又依赖订单服务获取下单状态。

这就是典型的服务雪崩效应。

我们当时的应对措施有三步:

  1. 加熔断限流组件 —— 我们选择了Sentinel。
  2. 给Feign添加Fallback兜底逻辑
  3. 异步+队列解耦部分业务流程(例如库存扣减)。

具体来说,在订单服务中加入Sentinel依赖:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

在Feign Client接口加上 fallback:

@FeignClient(name = "product-service", fallback = ProductClientFallBack.class)

实现fall back类:

@Component
public class ProductClientFallBack implements ProductServiceClient {
    @Override
    public Product getProductById(Long id) {
        return new Product();
    }
}

这样即使商品服务挂了,也能返回默认值或缓存值,避免整个系统崩溃。


路由网关登场:统一入口怎么玩?

当服务越拆越多之后,暴露出来的另一个问题就是:API地址越来越多,前端不知道应该访问哪个服务!

更麻烦的是权限控制、日志记录、IP白名单等功能需要在多个服务重复实现。

这时候我们就引入了Spring Cloud Gateway,作为统一入口层。

Gateway本身支持路由、过滤器链、限流等强大的功能。比如我们可以这样定义路由规则:

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=1

这样前端只需要访问/api/user/login,就会被自动转发到user-service的/login接口。

我们也实现了自定义的全局过滤器来做身份认证:

public class AuthFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (!isValidToken(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }
}

这一块虽然看起来简单,但调试起来还是有不少细节需要注意,比如Token校验失败后的响应格式要统一流程,不能出现有的服务返回JSON、有的服务跳转登录页的情况。


数据库设计:是共享还是独立?

这个问题争论了很久。最后我们采取的是“适度独立”。

什么意思呢?

对于高频率修改、强一致性要求的数据(如库存),我们采用独立数据库,各服务只能通过API交互。

而对于读多写少、低一致性的数据(如分类信息),我们允许一定限度的复制同步,避免频繁跨服务调用。

举个例子:

订单服务需要获取商品的基本信息,如果每次都要调用product-service,效率太低。

于是我们采用了数据冗余策略,在order-service中保存一份product简略信息快照:

@Data
@Entity
public class OrderProductSnapshot {
    private String productName;
    private BigDecimal price;
    private Integer quantity;
}

每当生成新订单时,就保存一次商品快照,这样即便后续商品信息更改,也不会影响历史订单的数据展示。

当然,快照也有缺点,比如占用存储、更新不及时等,但结合业务场景来看,利大于弊。


生产上线那些事:我们是怎么部署和维护的?

微服务拆得再好,部署不妥也是白搭。我们采用的方案是:

  1. Jenkins做持续集成,打包并上传到私有镜像仓库(Harbor);
  2. 使用Kubernetes集群管理服务部署;
  3. 每个服务部署两个Pod,配合健康检查确保高可用;
  4. 日志集中使用ELK(ElasticSearch + Logstash + Kibana)收集;
  5. 监控告警接入Prometheus + Grafana;
  6. 接口文档用Swagger整合所有服务API,方便前后端协作。

其中最头疼的一点是K8s的Service配置,尤其是在早期没有经验的情况下,有时候YAML写错了半天查不出问题。

后来我们整理了一份标准化的模板,包括资源限制、就绪检查、滚动更新策略等,才逐渐顺畅起来。


性能优化:从慢到快的关键几步

微服务化之后,不可避免地带来了额外的网络开销。为了提升性能,我们做了以下几件事:

  1. 引入缓存机制:Redis用于热点数据缓存(如热销商品、首页内容);
  2. 异步处理非关键路径:比如发送短信、邮件这些操作,全部丢进RabbitMQ;
  3. 数据库索引优化和连接池配置调整
  4. Feign调用增加超时控制,防止慢请求拖垮整个链路;
  5. 启用Zipkin做分布式追踪,快速定位性能瓶颈;
  6. 使用JVM性能分析工具(Arthas)排查内存泄漏和GC问题。

特别说一下Zipkin,它帮助我们发现了好多隐藏很深的延迟问题。

比如说某个服务明明响应很快,但整体链路却很慢,原来是有个下游服务在做批量任务时占用了线程资源,导致后续请求排队。


成果与收获:拆完之后真的更好了吗?

当然!

我们现在基本做到了:

✅ 各个服务完全解耦,独立开发、测试、部署;
✅ 整体系统吞吐量提升了30%以上;
✅ 故障范围可控,不会牵一发而动全身;
✅ 新成员更容易理解系统结构,入职更快上手;
✅ 实现了蓝绿部署、灰度发布的初步能力。

虽然初期投入了不少时间和精力,但从长远来看,这是值得的。


写给读者的经验总结

如果你正在准备微服务化改造或刚刚起步,以下是我在实践过程中积累的一些经验建议:

✅ 从小处试水,别上来就把大系统一顿拆

我们前期是先拆出一个服务,观察它的表现和稳定性,确认没问题后再推进其他模块。盲目拆分会带来巨大的维护成本。

✅ 服务命名要有规范,否则后期根本管不过来

我们当时统一使用{业务模块}-service的格式,如product-serviceorder-service,便于理解和查找。

✅ 分布式事务是个坑,慎用!推荐事件驱动或补偿机制

我们最早试图用Seata做分布式事务,结果复杂且效率低下。后来改为消息队列+本地事务表的方式,反而稳定多了。

✅ 日志和监控必须一开始就搞定

不然出了问题都不知道去哪查。一定要把日志统一采集,配上调用链追踪,否则你永远不知道请求卡在哪一个环节。

✅ 技术债一定要还,否则迟早反噬

比如有些API设计不合理,当时图省事没改,后面要改就得动很多地方,痛苦不已。


结语:微服务不是银弹,但正确使用可以带来质变

写到这里,我已经回忆起了那段边学边干、熬夜改Bug的日子。虽然累,但现在回头看,那次改造不仅提升了系统的可维护性,也让我自己对微服务的理解有了质的飞跃。

Spring Cloud生态非常庞大,涉及的技术点也非常多。但只要你从实际业务出发,坚持“先解决问题,再完善体系”的思路,就一定能走出一条适合自己的微服务之路。

希望这篇文章能帮你在微服务的道路上走得更稳、更远。欢迎留言交流,一起进步!


作者:一只热爱码代码的程序员 / 前端时间做过Java后端,现在专注微服务架构设计与研发

评论 0

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