认识微服务
简单了解

单体架构与分布式架构

单体架构,就是把所有功能像大杂烩一样全堆在一个项目里,好处是简单直接,开发完打个包就能上线,部署成本低,特别适合业务初期。但问题在于,所有代码都紧耦合在一起,改一小块可能得动全身,维护和升级越来越难,就像一栋楼只有一根承重柱,谁动谁负责。所以随着业务变复杂,我们就回归到“分而治之”这个基本逻辑——这就是分布式架构的出发点。

分布式架构按业务功能把系统拆成多个独立服务,每个服务各管一块,彼此解耦,这样谁负责哪个服务,就能独立开发、测试、部署和扩展,大大提升了迭代效率。
但拆分也带来了新问题:拆太细管理成本高,拆太粗又没意义,所以得思考拆分粒度;服务拆开了就得互相通信,怎么调用才可靠高效?调用关系一多,就容易乱,比如A调B、B调C,链路复杂了,出问题难排查。所以本质上,分布式不是一拆了之,而是要用一套标准和规范,比如定义好接口、治理好调用链、做好服务注册发现,来管理这种复杂性,让系统既灵活又可控。
微服务

微服务其实是在分布式架构的基础上,进一步回归到“高内聚、低耦合”这个软件设计的根本原则,给拆分服务这件事定了一套清晰的标准。
首先,它强调单一职责,也就是说每个微服务只干一件事,而且把它干好,比如用户管理、订单处理各自独立,这样逻辑清晰,改代码也不容易互相影响。
其次,要做到自治,就是从团队到技术栈、数据存储,甚至部署发布,每个服务都能独立运作,谁负责这个服务谁就说了算,真正实现“谁开发、谁维护”,提升交付效率。
然后是面向服务,每个微服务都通过统一的接口(比如HTTP+JSON)对外提供能力,不关心你用Java还是Python写的,语言和技术彻底解耦,协作更顺畅。
还有一个关键点是隔离性强,因为服务一多,一个服务卡了不能让整个系统瘫痪,所以要有熔断、降级、容错这些机制,就像楼里每层都有防火门,着火了也不会蔓延。
这些特性合在一起,本质上就是为了让系统更灵活、更稳定、更容易扩展。所以说,微服务不是简单地把项目拆开,而是一种经过良好设计的、有章可循的分布式架构方案。但光有理念不行,怎么落地?用什么技术?这时候大家就会选型,而在Java生态里,Spring Cloud之所以特别突出,就是因为它提供了一整套成熟的工具和规范——比如服务发现、配置中心、网关、熔断器等。
SpringCloud
SpringCloud是目前国内使用最广泛的微服务框架,集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。其中常见的组件包括:

另外,SpringCloud底层是依赖于SpringBoot的,并且有版本的兼容关系,如下:

服务拆分和远程调用
服务拆分

微服务拆分时的原则:
- 不同微服务,不要重复开发相同业务
- 微服务数据独立,不要访问其它微服务的数据库
- 微服务可以将自己的业务暴露为接口,供其它微服务调用
示例项目如下:

- 订单微服务和用户微服务都必须有各自的数据库,相互独立
- 订单服务和用户服务都对外暴露Restful的接口
- 订单服务如果需要查询用户信息,只能调用用户服务的Restful接口,不能查询用户数据库
实现远程调用
观察现象:

我们想要实现的是下图所示的功能,就需要在order-service中向user-service发起一个http的请求,调用http://localhost:8081/user/{userId}这个接口。

为什么需要订单模块发起http请求呢?因为当我们访问 http://localhost:8080/order/101 时,order-service 首先会根据订单 ID 查询订单的基本信息,然后看到 userId 是 1。这时,它意识到还需要用户的详细信息才能返回完整的订单数据。于是,它通过 HTTP 请求调用 http://localhost:8081/user/1 这个接口,将用户 ID 作为参数传递过去。user-service 收到请求后,根据用户 ID 查询数据库,找到对应的用户信息,并以 JSON 格式返回给 order-service。最后,order-service 将订单信息和用户信息合并在一起,形成一个完整的响应,返回给客户端。
可以怎么做呢?接下来让我们一步一步实现:
step1 在服务启动类中注册RestTemplate
因为是订单模块发起的http请求,所以需要在订单模块的服务启动类注册,谁发起http请求就在谁的服务启动类注册。
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
// ...
/**
* 在订单模块的服务启动类注册 RestTemplate
* @return
*/
@Bean
public RestTemplate restTemplate () {
return new RestTemplate();
}
}
step2 在service中实现远程调用
@Service
public class OrderService {
// ...
// 注入 restTemplate
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.远程查询user
// 2.1 url地址
// 根据调用的接口,所以将用户 ID 作为参数传递过去
String url = "http://localhost:8081/user/" + order.getUserId();
// 2.2 发起调用
// User.class 属于并不是一个对象实例,而是 Java 中的类对象,代表 User 这个类本身
// 它属于 Java 反射机制的一部分。我们调用 restTemplate.getForObject(url, User.class) 时,是告诉 Spring 的 RestTemplate
// 我向这个 URL 发起一个 HTTP 请求,拿到的是一个 JSON 格式的数据
// 你现在要帮我把这个 JSON 数据自动转换成一个 User 类型的对象。
User user = restTemplate.getForObject(url, User.class);
// 2.3 存入order
order.setUser(user);
// 4.返回
return order;
}
}
再次访问,就可以发现成了:

提供者与消费者

理解服务提供者和服务消费者这两个角色,其实它们不是固定标签,而是从“谁在调用谁”这个具体业务场景出发的相对概念,就像生活中你既是儿子又是父亲一样,取决于你看问题的角度。
最根本的一点是,在微服务架构里,每个服务都不可能独立完成所有事情,必然要和其他服务协作,于是就产生了调用关系。我们把被别人调用的那个服务叫做“服务提供者”,因为它对外提供了能力(对外暴露了接口);而主动发起调用的那个就是“服务消费者”,因为它需要消费别人的服务来完成自己的任务。举个例子,如果服务A调用了服务B,那A就是消费者,B就是提供者;但如果服务B为了完成A的请求,又去调用了服务C,那在B和C之间,B就变成了消费者,C成了提供者。所以你看,服务B同时扮演了两个角色——对A来说它是提供者,对C来说它又是消费者。
这说明在复杂的业务链路中,一个服务往往是“既供又消”的,这种角色的灵活性正是微服务协作的常态。我们设计系统时,不能简单地认为某个服务永远只是提供方或调用方,而要从整个调用链路的上下文去理解它的职责。
Eureka注册中心

- order-service在发起远程调用的时候,该如何得知user-service实例的ip地址和端口?
- 有多个user-service实例地址,order-service调用时该如何选择?
- order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?
没有什么是加入一个中间件解决不了的。解决这些问题的中间件名字就叫做 —— Eureka。是最广为人知的注册中心。
Eureka的结构和作用

接下来看图说话。
服务注册与服务发现得知服务提供者实例地址
问题1:order-service 如何得知 user-service 实例地址?
当 user-service 启动时,它会向 eureka-server 注册自己的信息(包括服务名称、IP 地址和端口号等),这个过程叫作服务注册。eureka-server 就像一个电话簿,保存了所有服务实例的地址信息,并将这些信息组织成一个映射关系表。当 order-service 需要调用 user-service 时,它会通过 eureka-client 向 eureka-server 发起请求,查询 user-service 的实例地址列表,这个过程叫作服务发现或服务拉取。如图所示,order-service 通过 eureka-client 拉取到 user-service 的多个实例地址(8081、8082、8083)。
负载均衡选择服务提供者的具体实例
问题2:order-service 如何从多个 user-service 实例中选择具体的实例?
拿到多个 user-service 实例地址后,order-service 需要决定具体调用哪一个。这时,它会使用负载均衡算法(见苍穹外卖DAY1-优快云博客的负载均衡部分)从实例列表中选中一个具体的实例地址,然后发起远程调用。例如,order-service 可能会选择 localhost:8081 进行调用。
心跳续约判断服务提供者实例是否健康或者宕机
问题3:order-service 如何得知某个 user-service 实例是否依然健康,是不是已经宕机?
为了确保调用的是健康的实例,每个 user-service 实例都会定期(默认每30秒一次)向 eureka-server 发送心跳信号,报告自己的状态,这称为心跳续约。如果某个 user-service 实例长时间没有发送心跳信号(通常超过90秒),eureka-server 就会认为该实例已经宕机,并将其从服务列表中剔除。这样一来,当 order-service 下次拉取服务列表时,就会自动排除掉那些已经故障的实例,从而保证调用的成功率和系统的稳定性。
统一的服务客户端
值得注意的是,无论是 order-service 还是 user-service,它们都可以既是服务提供者又是服务消费者。因此,Eureka 将服务注册、服务发现等功能统一封装到了 eureka-client 端,使得每个微服务都能方便地接入 Eureka 体系,实现自动化的服务治理。
自己动手实现
step1 搭建eureka-server
创建eureka-server服务
就是在父工程下创建一个子模块。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
@SpringBootApplication
@EnableEurekaServer // 开启eureka的注册中心功能
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
server:
port: 10086
spring:
application:
name: eureka-server
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
启动服务
启动微服务,然后在浏览器访问:http://127.0.0.1:10086,看到下面这个界面就是成了:

step2 服务注册
针对于user-service:
<!-- eureka-client依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
spring:
application:
name: userservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
然后设置并启动多个user-service实例:



通过图片可以佐证:当 user-service 启动时,它会向 eureka-server 注册自己的信息(包括服务名称、IP 地址和端口号等),这个过程叫作服务注册。eureka-server 就像一个电话簿,保存了所有服务实例的地址信息,并将这些信息组织成一个映射关系表。
step3 服务发现
针对于order-service:
服务发现、服务注册统一都封装在eureka-client依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
spring:
application:
name: orderservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
服务拉取和负载均衡
最后,我们要去eureka-server中拉取user-service服务的实例列表,并且实现负载均衡。不过这些动作不用我们去做,只需要添加一些注解即可。
在启动类创建restTemplate的地方加上:
@Bean
@LoadBalanced // 拉取实例列表,实现负载均衡
public RestTemplate restTemplate () {
return new RestTemplate();
}
Spring会自动帮助我们从eureka-server端,根据userservice这个服务名称,获取实例列表,而后完成负载均衡。
Ribbon负载均衡
为什么加@LoadBalanced注解就能实现负载均衡?底层其实是利用了一个名为Ribbon的组件,来实现负载均衡功能的。

第一步,图里的user-service有两个实例(8081和8082),在启动时,它们都会把自己的服务名(userservice)和当前地址(比如localhost:8081)注册到图中的eureka-server(服务注册中心)。所以,Eureka手里就有一张完整的服务清单,知道userservice这个服务对应着哪几个活的实例。
第二步,当order-service需要调用user-service的接口时(比如获取用户信息),它不会直接写死一个IP地址,而是发起一个面向服务名的请求,比如http://userservice/user/1。这里的userservice就是一个逻辑名字,而不是一个具体的地址。
第三步,这个请求并不会直接发出去,而是被Ribbon这个客户端负载均衡组件拦截了。Ribbon会去联系Eureka服务器,说:“嘿,告诉我所有名叫userservice的、健康的实例列表。” Eureka服务器就把准备好的列表(localhost:8081, localhost:8082)返回给它。
第四步,Ribbon现在手里有了一份可用服务器列表。接下来,它按照配置的负载均衡规则(默认是轮询),从列表里选出一个实例,比如这次它轮询选择了8081。然后,Ribbon会帮我们把最开始那个面向服务名userservice的请求,就地改写成面向具体地址http://localhost:8081/user/1的请求,并真正把这个请求发出去。最终,order-service就成功调用了user-service在8081端口上的实例。
这个流程的核心思想是:将“服务发现”(找地址)和“负载均衡”(选一台)这两个动作,从应用程序代码中剥离出来,由客户端(Ribbon)在发起请求前自动完成。这样做的好处是保证了微服务之间的调用是弹性的、高可用的,调用方不再关心被调用服务的具体部署细节,即使某个实例宕机,Ribbon下次也会选择另一个健康的实例,从而实现了系统的容错和高可用。
源码解析
第一步:请求被拦截(LoadBalancerInterceptor)
当我们用RestTemplate发送一个像http://user-service/user/8这样的请求时,这个请求并不会直接发出去。它会被一个叫LoadBalancerInterceptor的拦截器给“拦下来”。这个拦截器的intercept方法干了三件关键事:
-
解析服务名:它把请求的URL拆开,取出主机名部分,也就是
user-service。这个user-service不是一个真正的网址,而是一个代表服务的逻辑标识。 -
委托给负载均衡器:它拿着这个服务名
user-service,去调用loadBalancerClient.execute()方法,说:“我这儿有个给user-service的请求,你帮我处理一下后续的发送。” -
重构请求:后续的真正操作,就是把
http://user-service/user/8这个地址,替换成一个真实的、像http://localhost:8081/user/8这样的地址再发出去。
第二步:负载均衡器决策(RibbonLoadBalancerClient)
loadBalancerClient.execute()方法是真正的决策中心,它内部又做了两件事:
-
获取服务清单:它通过
getLoadBalancer(serviceId),根据服务名user-service,去找到对应的负载均衡器(ILoadBalancer)。这个负载均衡器可不是空的,它早就从Eureka注册中心拉取并维护了一份健康的、名叫user-service的所有实例的清单(比如127.0.0.1:8081,127.0.0.1:8082)。 -
挑选实例:它通过
getServer(loadBalancer)方法,从这份清单里挑一个实例出来。这个选择不是随机的,而是由负载均衡规则(IRule) 决定的。从源码里我们看到,它调用了loadBalancer.chooseServer("default")。
第三步:执行负载均衡规则(IRule - 默认轮询)
chooseServer方法最终会调用到rule.choose(key)。我们从源码里清晰地看到,默认的规则(rule)是一个RoundRobinRule实例。
-
RoundRobinRule,顾名思义,就是轮询。它内部维护了一个计数器,每次选择时,按顺序依次选择下一个服务器。这就完美解释了为什么第一次跟踪时选到了8082,第二次再请求就选到了8081,实现了负载均衡。
总结来看,就是SpringCloudRibbon的底层采用了一个拦截器,拦截了RestTemplate发出的请求,对地址做了修改。

负载均衡策略
负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:

不同规则的含义如下:
最基础的就是RoundRobinRule,也就是简单的轮着来,一台一台过,这是Ribbon默认的方式,实现简单、压力分布也比较均匀。而RandomRule就更直接了,完全随机选,相当于抛骰子决定。WeightedResponseTimeRule则认为响应快的服务器应该多承担任务,所以会根据每台服务器的平均响应时间动态算出一个权重,响应越慢权重越低,选择概率就越小。AvailabilityFilteringRule和BestAvailableRule都考虑了服务器的“健康度”和“压力”,比如连续失败三次就会被暂时拉黑(短路),还会避开连接数太多的服务器,确保不会雪上加霜。RetryRule是在选服务器时带了个重试机制,如果第一次选的服务器连不上,它不会立刻放弃,而是再换一个试试,提高了请求的成功率。最后是ZoneAvoidanceRule,它引入了“区域”的概念,比如同一个机房内的服务器优先调用,这样可以减少跨区域通信的延迟,提升整体效率。
默认的实现就是ZoneAvoidanceRule,是一种轮询方案。
自定义负载均衡策略
有两种方式,一种是在启动类定义一个新的IRule:
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
//...
/**
* 定义IRule实现可以修改负载均衡规则 - 启动类
* @return
*/
public IRule randomRule() {
return new RandomRule();
}
}
第二种就是在配置文件中,可以添加新的配置也可以修改规则:
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
注意,一般用默认的负载均衡规则,不做修改。
饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
ribbon:
eager-load:
enabled: true
clients: userservice
Nacos注册中心
中国人有自己的注册中心,支持国产。SpringCloudAlibaba也推出了一个名为Nacos的注册中心,国内公司喜欢用。
服务注册到nacos
Nacos与Eureka没什么区别。
startup.cmd -m standalone
父pom的依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
子pom的依赖,注意注释掉eureka的依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
添加nacos地址,也需要注释掉eureka的地址:
spring:
cloud:
nacos:
server-addr: localhost:8849
登录nacos之后,就可以查看到微服务信息:

服务分级存储模型

一个服务,比如我们的user-service,可以有多个实例运行在不同的机器上。这些实例就像是服务的小分队,每个小分队都有自己的地址和端口,比如127.0.0.1:8081、127.0.0.1:8082和127.0.0.1:8083。这些实例分布在全国各地的不同机房,比如上海机房和杭州机房。
现在,想象一下,如果我们把这些实例简单地放在一起,可能会导致混乱和效率低下。为了解决这个问题,Nacos引入了一个概念——集群。Nacos将同一机房内的实例划分为一个集群。也就是说,所有在上海机房的user-service实例(如127.0.0.1:8081和127.0.0.1:8082)被归为一个集群,而杭州机房的实例(如127.0.0.1:8083)则属于另一个集群。

这样一来,我们就形成了一个分级模型:一个服务可以包含多个集群,每个集群下又可以有多个实例。这种结构的好处在于,当微服务互相访问时,它们应该尽可能访问同集群的实例,因为本地访问速度更快,网络延迟更低。例如,如果杭州机房内的order-service需要调用user-service,它会优先选择同机房的user-service实例,只有当本集群内的实例不可用时,才会去访问其他集群的实例。
配置集群(以user-service为例)
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称
-Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH
我们自定义了两个集群:

同集群优先的负载均衡
ZoneAvoidanceRule引入了“区域”的概念,比如同一个机房内的服务器优先调用,这样可以减少跨区域通信的延迟,但是默认的ZoneAvoidanceRule并不能实现根据同集群优先来实现负载均衡。因此Nacos中提供了一个NacosRule的实现,可以优先从同集群中挑选实例。
order-service添加集群配置:
spring:
cloud:
nacos:
server-addr: localhost:8849
discovery:
cluster-name: HZ # 集群名称
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
权重配置
在实际部署中,我们经常会遇到一个很现实的问题:服务器的硬件性能并不是完全一样的,有的机器配置高、性能好,有的可能稍微老旧一些、性能弱一点。但问题来了,默认情况下,Nacos的负载均衡策略(也就是NacosRule)在同一个集群内是随机选择实例的,它不会去判断哪台机器强、哪台弱,相当于“一视同仁”。这就可能导致高性能机器没发挥出优势,而低性能机器反而被压垮。为了解决这个问题,Nacos提供了一个非常实用的机制——权重配置。我们可以给每个服务实例设置一个权重值,权重越高,被选中的概率就越大。如果权重修改为0,则该实例永远不会被访问。

环境隔离
默认情况下,所有service、data、group都在同一个namespace,名为public。

为了避免各个环境之间相互干扰,防止开发中的代码误调用生产服务,或者测试流量冲击线上系统,造成数据混乱甚至事故。
为了解决这个问题,Nacos 提供了一个叫 namespace(命名空间)的机制,它就是用来做环境隔离的。你可以把 Nacos 想象成一个大容器,这个容器里可以有多个独立的“小房间”,每个“小房间”就是一个 namespace。比如我们可以创建 dev、test、prod 三个 namespace,分别对应开发、测试和生产环境。
在每个 namespace 里面,又能定义自己的服务(service)、分组(group)等资源。最关键的是,不同 namespace 之间的服务是完全隔离的,互相看不见,也调用不到。比如你在 dev 环境注册了一个 user-service,它只在 dev 这个“房间”里存在,prod 环境的服务发现机制是根本发现不了它的。
因为namespace不同,所以同时控制台会报错。

Nacos与Eureka的区别

在一个微服务系统里,服务是动态变化的——可能上线、下线、扩容、宕机。那我们要做的核心事情就是两件:第一,让服务能被发现;第二,要能准确知道它是活着还是挂了。Nacos 和 Eureka 其实都是为了解决这个问题而生的,所以它们最基本的能力是一样的,比如都支持服务注册和拉取,也都允许服务通过发送心跳来告诉注册中心“我还活着”,这是它们的共同点。
但 Nacos 更进一步,它把服务实例分成了两种类型:临时实例和非临时实例。这背后其实体现了不同的设计哲学。临时实例是默认类型,它的逻辑是“你不按时打卡(发心跳),就认为你离职了”——如果一个临时实例宕机了,超过一定时间没发心跳,Nacos 就会把它从服务列表里踢出去,这跟 Eureka 的机制很像。而另一种是非临时实例,也叫永久实例,它的特点是:哪怕你宕机了,也不会被自动移除,必须手动删除或更新。这种模式适合那些我们认为非常关键、不能轻易消失的服务,比如配置中心本身或者一些物理设备接入的场景。
更重要的是,Nacos 不仅支持心跳检测,还支持服务端主动去探测服务状态,尤其是对非临时实例,默认就是用这种更可靠的主动检测方式。这就比单纯依赖客户端心跳更健壮,因为有时候不是服务挂了,而是网络抖动导致心跳丢了,Nacos 能更准确地区分这种情况。
另外,Nacos 还有个优势是支持服务列表变更的消息推送机制,也就是说一旦有服务上下线,其他服务能更快收到通知,而不是像 Eureka 那样靠定时拉取,更新延迟更大。
最后从一致性协议来看,Eureka 是典型的 AP 系统(高可用优先,容忍数据短暂不一致),而 Nacos 默认也是 AP,但有一个非常关键的区别:当集群里存在非临时实例时,Nacos 会自动切换成 CP 模式(一致性优先),确保所有节点看到的服务状态是一致的。这意味着 Nacos 更灵活,既能满足高可用需求,也能在需要强一致的场景下提供保障。
设置非临时实例的配置如下:
spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为非临时实例

2200

被折叠的 条评论
为什么被折叠?



