Java 微服务
一、REST
REST,指的是 Re presentational S tate Transfer,表现层资源状态转移,其中涉及下面三个概念
-
表现层 ,资源的表现形式,比如文本可以通过 txt、HTML、JSON 等形式甚至二进制的方式表现
-
资源 ,网络上的一切都可以抽象为资源,对应唯一资源标识符(URI)
-
状态转移 ,访问 URI,使用 HTTP 进行资源操作,对资源的增删查改都涉及到资源的状态转移
RESTful API 是指借鉴 REST 风格的接口设计,主要涉及两个概念
-
资源 ,可以是音频、图片、视频、文本等。除二进制数据外,其他更多通过 JSON 的方式传递,通常从数据库中取到
-
统一接口 ,RESTful 的接口,对于资源的具体操作只能从 HTTP 请求方法中看出,请求中只含有资源的种类,简单来说:接口只有名词,动词(即资源操作)体现在请求方法
REST 方法对于请求方法的要求较高,但是有的浏览器对于除 GET、POST 之外的请求方法支持不好;请求方法不在请求中直接体现,需要通过抓包的方式查看。所以依据情况选择设计风格
RestTemplate 是 Spring 提供的同步 HTTP 客户端,一般通过它调用 RESTful 的接口
二、服务注册与发现
2.1 CAP 理论
一个分布式系统,存在下面的关系图

-
P ,Partition Tolerance,分区容错性。这里的分区指的是网络分区,指的是分布式系统中各个节点沟通不畅的情况(是节点间的网络发生问题,而不是节点本身挂掉)
-
C ,Consistency,一致性,各个节点之间持有的数据是相同的
-
A ,Availability,可用性,不管何时对集群请求,总是能得到结果(正确性不保证)
根据 CAP 理论,C、A、P 是不可全部实现的
在实际操作中,网络波动是不可避免地,所以会选择实现分区容错性,那么接下来有两种架构方向:AP、CP
AP 即保证可用性,在出现网络分区时,为了保证数据可用性,仍然返回节点当前持有的数据(这意味着数据可能是错误的,或者说不是最新的),后续通过异步同步数据,最后实现数据的统一性(即 AP 架构放弃了强一致性,最终还是会实现数据一致的)
CP 即保证一致性,在发生网络分区时,选择不返回数据,等到网络分区结束并且数据同步后,再进行请求处理、数据返回
2.2 常见的注册中心
-
Eureka,AP
-
Zookeeper,CP
-
Nacos,AP/CP,默认 AP
2.3 负载均衡
Spring Cloud LoadBalancer 是 Spring Cloud 内置的负载均衡器,默认通过轮询的方式实现负载均衡(也可以改为随机的方式)
使用
给 RestTemplate 的配置加上 @LoadBalanced 注解,使用 RestTemplate 发送请求时,通过 service-name 代替 ip:port 的方式配置请求即可自动完成负载均衡的服务调用
Java
@Component
public class BeanConfig {
@Bean
@LoadBalanced // 通过 Spring Cloud Load Balancer 实现负载均衡
RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
请求构造
Java
String url = "http://product-service/product/" + orderInfo.getProductId();
Spring Cloud LoadBalancer 是通过 LoadBalanceInterceptor 拦截实现的,向服务注册中心获取服务并将服务名替换为 ip:port 的形式
需要注意的是,负载均衡的引入不能重复,不可以既导入 Enreka,又导入 Nacos
2.4 Eureka
2.4.1 Eureka Server
-
导入依赖
XML<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> -
在微服务 SpringBoot 入口启动服务注册
Java@EnableEurekaServer //开启 Eureka 服务注册 @SpringBootApplication public class EurekaServiceApplication { public static void main(String[] args){ SpringApplication.*run*(EurekaServiceApplication.class, args); } -
配置 Eureka 服务注册地址
YAMLeureka: instance: hostname: localhost client: fetch-registry: false * *register-with-eureka: false * *service-url: * *defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
其中,eureka 几项配置为:
-
fetch-registry,是否从其他 Eureka 节点获取注册信息,默认是 true,如果是单点 Eureka 的话,设置为 false -
register-with-eureka,是否将当前服务注册到 Eureka,默认是 true。对于一个 Eureka Server 本身来说,不需要进行注册,设置为 false -
defaultZone,设置服务注册地址,后续注册服务、获取服务列表都需要使用这个 URL
2.4.2 Eureka Client
-
导入依赖
XML<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> -
配置服务注册地址,注册自身服务
YAMLeureka: client: service-url: defaultZone: http://127.0.0.1:10010/eureka -
RestTemplate 开启负载均衡能力
Java@Configuration public class BeanConfig { @Bean @LoadBalanced // 通过 Spring Cloud Load Balancer 实现负载均衡 public RestTemplate getRestTemplate(){ return new RestTemplate(); } } -
从服务中心获取服务
JavaList<ServiceInstance> servers = discoveryClient.getInstances("product-service"); //获取服务实例列表 EurekaServiceInstance server = (EurekaServiceInstance)servers.get(0); //去列表中取第一个 String url = server.getUri() + "/product/" + orderInfo.getProductId(); //根据实例信息拼接请求,后续可以通过 RestTemplate 发起调用
2.5 Nacos
2.5.1 Docker 部署
Dockerfile
sudo docker run -d --name nacos -p 8848:8848 -p 9848:9848 -e MODE=standalone nacos/nacos-server:v2.3.2
使用 docker 部署后,进入容器可以看到 Nacos 的运行目录,如果出现问题时往往需要在这里进行处理
Shell
diinkikot@DESKTOP-EI61K7J:~/nacos/bin$ sudo docker exec -it nacos /bin/bash
[root@79168aedf03c nacos]# ls
LICENSE NOTICE bin conf data derby.log logs start.out target work
或者也可以在 Linux 通过 zip 压缩包解压后运行,目录如下
Shell
diinkikot@DESKTOP-EI61K7J:~/nacos$ ls
LICENSE LICENSE:Zone.Identifier NOTICE NOTICE:Zone.Identifier bin conf data logs target
其中,bin 中包含了 nacos 启动、停止脚本,conf 中包含配置文件(如配置端口,默认为 8848,对应需同时保证 9848、9848 未被占用,data 中为服务数据
2.5.2 快速上手
-
导入 Nacos 依赖
XML<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-loadbalancer</artifactId> </dependency> -
配置 Nacos 地址
YAML# application.yaml spring: cloud: nacos: discovery: server-addr: localhost:8848 -
进行远程调用:与 Eureka 不同,Nacos 不用通过微服务的方式运行,而是和 MySQL、Redis 一样单独运行;并且负载均衡的调用和 Eureka 的相同,因为都是通过 Spring Cloud LoadBalancer 实现负载均衡器 - 请求的转接
-
RestTemplate 开启负载均衡能力
Java@Configuration public class BeanConfig { @Bean @LoadBalanced // 通过 Spring Cloud Load Balancer 实现负载均衡 public RestTemplate getRestTemplate(){ return new RestTemplate(); } } -
从服务中心获取服务
JavaList<ServiceInstance> servers = discoveryClient.getInstances("product-service"); //获取服务实例列表 EurekaServiceInstance server = (EurekaServiceInstance)servers.get(0); //去列表中取第一个 String url = server.getUri() + "/product/" + orderInfo.getProductId(); //根据实例信息拼接请求,后续可以通过 RestTemplate 发起调用
-
2.5.3 Nacos 负载均衡
Spring Cloud LoadBalancer 有自己的负载均衡实现方式,所以需要额外的配置让 Nacos 的权重配置、同集群优先访问生效
YAMLspring: cloud: loadbalancer: nacos: enabled: true
-
Nacos 网页管理页面中可以下线服务、配置权重。
-
Nacos 会把同一个机房中的机器上的服务认为在同一个集群上,这样这些节点之间的访问是要快于跨局域网的,即某种程度上可以是认为是同一机房优先访问
配置集群名称
YAMLspring: cloud: discovery: cluster-name: SH
2.5.4 Nacos 健康检查
2.5.4.1 健康检查机制
-
客户端主动上报,即客户端主动发送信息表明尚未下线,默认间隔 5s 发送,30s 内无响应咋认为节点下线删除节点
-
服务端反向探测,服务端发送信息主动确认客户端的情况,20s 没有反应会将节点标记为 unhealthy,而不会下线节点
2.5.4.2 临时实例和非临时实例
Nacos 对于临时实例使用客户端主动上报,而非临时实例使用的是服务端反向探测。节点默认是临时实例,如果需要设置为非临时实例,需要如下配置
YAML
spring:
cloud:
discovery:
ephemeral: false
报错:当前节点为临时实例不可转换为非临时实例,或者当前节点为非临时实例不可转换为临时实例
原因是 Nacos 会记录每个实例的 ip、端口,在两者都没有更改时是不允许转换的。
解决方式:
更改端口
以 docker 为例,进入容器后,删除
/data/protocol中的内容,之后重启容器,即可以完成转换实例状态正常,但是显示 unhealthy
对于非临时节点来说,Nacos 的健康检查方式是主动发送信息确认。但是对于通过阿里云内部 MSE 托管的 Nacos 来说,为了安全规范禁止服务端主动向客户端发送请求,所以误认为非临时实例下线
解决方式:将实例设置为非临时实例
2.5.5 Nacos 服务环境隔离
通过 namespace 进行,不同命名空间的服务不能互相访问(获取实例列表)
YAML
spring:
cloud:
discovery:
namespace: namespace-id # 在 nacos 中建立 namespace,并将命名空间 id 放在这里
2.5.6 Nacos 配置中心
2.5.6.1 Nacos 服务器建立配置文件
-
目前支持的方式有 yaml、properties(其他选项仅限于有)
-
Data-Id,对应到项目名称,比如 product-service、order-service
2.5.6.2 客户端服务器获取配置
-
客户端相应配置都要放在 bootstrap.* 中,文件类型需要和 Nacos 服务端配置文件的类型相同,默认需要指定的有项目名称和 Nacos 服务器地址。Nacos 的服务注册中心地址和配置中心地址是在两个不同的项中
YAMLspring: application: name: product-service profiles: active: dev cloud: nacos: config: server-addr: localhost:8848 -
导入 Nacos Config 依赖
XML<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!-- SpringCloud 2020.*之后版本需要引入bootstrap--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency> -
在启动微服务时,会首先通过 bootstrap.yaml 尝试从 Nacos 获取配置,之后和 application.yaml/.properties 的配置进行合并
2.5.6.3 配置中心命名空间
配置中心的命名空间和服务发现的命名空间是两个不同的命令空间,但默认都是 public,同时也都可以起到隔离的作用
YAML
spring:
cloud:
nacos:
config:
namespace: 6e61929d-a9bd-4504-9031-6ca57bc6c0b0
2.5.7 Data Id
前面提到了 Nacos 服务端的配置文件的 Data Id。Nacos 读取配置文件的完整结构为
Plain
${spring.application.name}[-${spring.profiles.active}].[${spring.cloud.nacos.config.file-extension}]
Nacos 按照优先级读取下面三个文件
-
KaTeX parse error: Undefined control sequence: \- at position 30: ...ication\.name\}\̲-̲{spring.profiles.active}.${spring.cloud.nacos.config.file_extension}
-
KaTeX parse error: Expected group as argument to '\.' at end of input: ...ation\.name\}\.{spring.cloud.nacos.config.file_extension}
当配置内容重复时,会优先读取优先级更高的 Data Id 对应的配置文件的内容
2.5.8 AP 与 CP
Nacos 既可以 AP 架构,也可以 CP 架构,对于临时节点采取 AP 的策略,对于非临时节点采取 CP 策略
三、远程调用
3.1 快速上手
RPC ,Remote Process Calling,是指像调用本机方法一样调用远程的方法,在 Spring 中常使用 OpenFeign
-
导入 OpenFeign 依赖
XML<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> -
给启动类增加 @EnableFeignClients 注解,开启 OpenFeign 功能
Java@EnableFeignClients @SpringBootApplication public class OrderServiceApplication { public static void main(String[] args){ SpringApplication.*run*(OrderServiceApplication.class, args); } } -
编写 OpenFeign 客户端,通过 SpringMVC 的方式指定请求路径、请求参数、服务名称(Spring 自行通过 LoadBalancer 进行负载均衡),接口需要使用
@FeignClient修饰Java@FeignClient(path="/product", value="product-service") public interface ProductApi { @RequestMapping("/get_num") public Integer getNum(Integer num); @RequestMapping("/test1") public String test1(@RequestParam("num1") Integer num1, @RequestParam("num2") Integer num2); @RequestMapping("/test2") public String test2(ProductInfo productInfo); @RequestMapping("/{id}") public ProductInfo selectProductById(@PathVariable("id") Integer id); @RequestMapping("/test3") public String test3(@RequestBody ProductInfo productInfo); }
其中,value 指定服务名称,用于服务发现、负载均衡;path 为所有请求的前缀路径,对应 @RequestMapping 作用于 Controller 类时的效果
-
远程调用
JavaProductApi productApi; @RequestMapping("get_num") Integer getNum(Integer num){ *log*.info(num + ""); return productApi.getNum(num); }
3.2 参数传递
FeignClient 起到了一个代替我们手动拼接字符串发送请求的作用,所以说参数对应的关系依然是存在的,需要满足 Spring MVC 的参数传递规则,例如
-
单参数传递
Java@RequestMapping("/get_num") public Integer getNum(Integer num) -
多参数,需要使用
@RequestParam绑定参数(默认 required = true)Java@RequestMapping("/test1") public String test1(@RequestParam("num1") Integer num1, @RequestParam("num2") Integer num2); -
传递对象,如果目的是将对象作为请求字符串的一部分,需要加上
@SpringQueryMap注解,自动将类序列化后加入请求参数;否则默认将对象加入请求正文Java@RequestMapping("/test2") public String test2(@SpringQueryMap ProductInfo productInfo); -
传递 JSON,放在请求正文中。在服务消费者接受请求,从正文中取数据,使用
@RequestBody;服务消费者调用 Feign 客户端,同样使用@RequestBody,不过这里的意思是告诉 FeignClient 进行远程调用的时候,将数据拼接到请求正文中。一个是取数据,一个是放数据Java//ServiceConsumerController or ServiceProvidorController @RequestMapping("/test3") public String test3(@RequestBody ProductInfo productInfo){ //FeignClient @RequestMapping("/test3") public String test3(@RequestBody ProductInfo productInfo);
3.3 抽取式和继承式
按照上面的方式,我们要根据服务提供方的接口设计单独"描绘"出来一个 FeignClient 客户端,同时不同服务调用这个服务时,都需要重复一遍上述步骤,多出了很多代码。
所以我们选择把这些接口单独抽出来,放到一个 api 模块中,这个模块只放接口和 DTO,由服务提供者实现接口中的内容,这样就解决了一个问题:每个服务调用者都要"描绘"一遍服务提供者的 Controller
现在还有一个问题,每个服务调用者的 FeighClient 呢?有两种代码构建方式
继承式 是 OpenFeign 推荐的方式,将存放接口的模块单独打成 jar 包,放到本地仓库中,然后需要调用服务的模块引入,自己的 FeignClient(使用 @FeignClient 修饰的类)继承这个接口,这样就完成了 FeignClient 的编写

抽取式 使更多实际使用的方式,在 api 模块中定义接口时,将这个接口作为 FeignClient,使用 @FeignClient 注解修饰并由服务提供者实现,将这个模块打 jar 包后分发给服务调用者;服务调用者必须要再自己定义 FeignClient,只需要在自己的启动类处通过 @EnableFeignClients 的方式指定扫描路径或者客户端类即可
Java
@EnableFeignClients(basePackages = {"com.wjl.api"}) //指定扫描路径
@EnableFeignClients(clients = {ProductApi.class}) //指定要加载的 feign client 客户端
四、网关
前面介绍了服务注册与发现、负载均衡、远程调用,还缺少一个网关,作为整个后端服务的入口。
网关可以起到如下作用
-
对请求进行鉴权过滤
-
一切请求经过网关,起到动态路由的作用
-
流量限制,按照后端服务能接受的流量放行请求
网关我们介绍 Spring Cloud Gateway,和 Eureka 一样作为一个服务在后端运行
4.1 快速上手
-
导入依赖
XML*<!--网关-->* <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> *<!--基于nacos实现服务发现依赖-->* <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> *<!--负载均衡-->* <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> -
添加路由配置
YAMLserver: port: 10030 spring: application: name: gateway cloud: nacos: discovery: server-addr: localhost:8848 gateway: routes: - id: product-service uri: lb://product-service predicates: - Path=/product/** *#filters:* * #- AddRequestParameter=username, wjl* * *- id: order-service uri: lb://order-service predicates: - Path=/order/** # 路径匹配规则 - After=2025-01-01T00:00:00.000+08:00[Asia/Shanghai] # 时间限制规则-
server.port,网关的ip、端口 -
spring.cloud.gateway.routes,路由配置,后续通过 yaml 数组的语法添加多条路由配置-
id,路由id,保证唯一即可 -
uri,只能是普通 uri ;或者通过lb://service-name的方式,到注册中心获取实例列表,由 Spring LoadBalancer 负载均衡的调用(lb 代表负载均衡) -
predicates,路由条件,只有符合要求要求的请求会被路由到相应服务
-
-
4.2 Gateway Filter Factories
Predicates 决定了哪些请求需要被处理,而 filter 负责对请求处理前后增添额外的逻辑,包含两种
-
GatewayFilter,作用于一个或一组路由
YAMLspring: cloud: gateway: routes: - id: product-service uri: lb://product-service predicates: - Path=/product/** *filters:* * - AddRequestParameter=username, wjl*GatewayFilter 提供了很多 Filter,
AddRequestParameter会在所有请求中加上参数,可以后端直接获取这种方式作用于部分接口,可以通过
default-filters作用于所有路由接口YAMLspring: cloud: gateway: default-filters: - AddRequestParameter=username, wjl routes: - id: product-service uri: lb://product-service predicates: - Path=/product/** -
GlobalFilter,作用于所有路由
添加依赖
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
添加配置
YAML
spring:
cloud:
gateway:
metrics:
enabled: true
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
shutdown:
enabled: true