重点⚠️⚠️
- ⚠️源码在文章末尾⚠️
一、概述
SpringCloud组成部分
SpringCloud主流组件
组件选型,结合SpringCloudAlibaba
二、构建项目
1.父工程构建注意事项
- 构建方式使用
<packaging>pom</packaging>
, 用于管理子工程的依赖和版本,并不会生成真实的构建物 - 编码规范
2.Alibaba工程规约
可以参考文档:阿里巴巴代码规范-黄山版
- Alibaba工程规约,主要指分层,和各层传输对象的命名方式
- 项目分层:
- 命名规约
- 个人理解
- VO
VO作为Controller层向外展示的对象
VO在进入service层之前,需要转换为DTO - DTO
DTO作为service向controller返回的对象
DTO也可以作为Controller方法的入参(保存,更新等)
DTO也作为service层方法的入参
DTO在进入dao层之前需要转换为DO - DO
DO作为dao层的方法入参
DO也作为dao层的方法出参 - Query
Query作为controller方法的入参(多个查询参数的组合)
Query在service层不用转换,可以直接作为service方法的入参
Query在进入dao层之前转换为DO
3.如果lombok失效,查看一下下面是否勾选(每个模块都要勾选)
三、Eureka
充当服务注册发现中心,Zookeeper和Nacos也可以
1.概述
- Eureka包含两个组件
- Eureka Server
提供服务注册 - Eureka Client
通过注册中心发现服务
2.搭建单机版Eureka Server
- 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
- 添加yml配置
- application.yml
server:
port: 8001
spring:
profiles:
active: eureka
application:
name: cloud-eureka-service
- application-eureka.yml
spring:
config:
# 引入其他配置文件
import: application.yml
eureka:
instance:
hostname: localhost
client:
# 表示不注册eureka自己
register-with-eureka: false
# 维护服务实例,不需要检索服务
fetch-registry: false
service-url:
# 设置eureka的地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka
3.启动类开启Eureka
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApp {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApp.class,args);
}
}
- 检验是否搭建好
- 输入localhost:8001查看
3.微服务提供者,添加至EurekaClient端
- 添加pom依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 添加yml文件
spring:
config:
# 引入其他配置文件
import: application.yml
eureka:
instance:
hostname: localhost
client:
# 表示不注册eureka自己
register-with-eureka: true
# 维护服务实例,不需要检索服务
fetch-registry: true
service-url:
# 设置eureka的地址
defaultZone: http://${eureka.instance.hostname}:8001/eureka
- 启动类添加client注解
@SpringBootApplication
@EnableEurekaClient
public class PaymentApp {
public static void main(String[] args) {
SpringApplication.run(PaymentApp.class,args);
}
}
4.微服务消费者,添加至EurekaClient端
和微服务提供者一样的步骤
5.搭建集群版Eureka Server
- server端
- 再搭建一个eureka server,设置成彼此的eureka地址即可
- client端
- 将client端的配置,eureka改成集群地址即可
- 效果
6.服务提供者搭建集群版
- 多个服务的服务名应一样,地址不一样(IP,端口)
7.消费者通过微服务调用
在本次演示中,消费者使用RestTemplate调用,底层也是Http协议
- 在不使用微服务的情况下,调用方式如下
- 配置RestTemplate,RestTemplate主要用于服务之间的HTTP请求调用
@Configuration
public class ApplicationContextConfig {
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
- 配置调用URL
provider:
payment:
host: http://localhost:9000
- 程序中调用
@Value("${provider.payment.host}")
private String paymentHost;
@Override
public PaymentDTO getPaymentById(Integer id) {
// 定义要发送的请求 URL,使用占位符来表示路径参数
String url = paymentHost + "/payment/{id}";
// 构建请求并获取响应
ResponseEntity<JSONObject> response = restTemplate.getForEntity(url, JSONObject.class, id);
JSONObject responseBodyJson = response.getBody();
JSONObject dataJson = responseBodyJson.getJSONObject("data");
String dataString = dataJson.toJSONString();
PaymentDTO paymentDTO = JSONObject.parseObject(dataString, PaymentDTO.class);
return paymentDTO;
}
- 使用微服务调用方式如下:
- 需要将RestTemplate配置为负载均衡,不然单依靠微服务名称无法寻找到到底是哪个提供者提供了服务
@Configuration
public class ApplicationContextConfig {
/**
* @LoadBalanced 赋予RestTemplate负载均衡能力
* @param
* @return org.springframework.web.client.RestTemplate
*/
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
- 需要修改yml配置,将远程的ip换成微服务名称
provider:
payment:
host: http://CLOUD-PAYMENT-SERVICE
8.actuator信息完善,设置EurekaServer别名
- 将Eureka的yml配置修改一下,添加一个instance-id
spring:
config:
# 引入其他配置文件
import: application.yml
eureka:
instance:
instance-id: payment9000
client:
# 表示不注册eureka自己
register-with-eureka: true
# 维护服务实例,不需要检索服务
fetch-registry: true
service-url:
# 设置eureka的地址
defaultZone: http://eureka8001.com:8001/eureka,http://eureka8002.com:8002/eureka
- 修改后的名称
9.通过接口查找Eureka的注册信息
- 首先在启动类添加注解@EnableDiscoveryClient
- 该注解的作用是:让注册中心能够发现,扫描到该服务
- 和@EnableEurekaClient的区别:@EnableEurekaClient针对于Eureka注册中心,而@EnableDiscoveryClient针对其他注册中心(Zookeeper,Nacos)
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentApp {
public static void main(String[] args) {
SpringApplication.run(PaymentApp.class,args);
}
}
- 业务代码
@GetMapping("discovery-service")
public ResultVO discoveryService(){
try {
// 获取微服务清单
List<String> services = discoveryClient.getServices();
for (String service : services){
List<ServiceInstance> instances = discoveryClient.getInstances(service);
for (ServiceInstance instance : instances){
log.info("service : {} , host : {} ,port : {}, uri: {}",instance.getServiceId(),instance.getHost(),instance.getPort(),instance.getUri());
}
}
return ResultVO.success();
} catch (Exception e) {
return ResultVO.failure(ResultCode.EUREKA_ERROR);
}
}
10.保护模式
某时刻某个微服务不可用了(有可能因为网络分区导致短暂的不可用),Eureka不会立即注销该服务,而是将该服务的信息保存
可以取消自我保护,具体可以百度,这里不做过多解释
四、ZooKeeper
⚠️注意⚠️
如果使用docker启动zookeeper,记住把端口映射,并且关闭防火墙(或者开放端口)
1.概述
- 可跳转到文章了解:zookeeper详解
2.ZooKeeper
- 修改配置文件,application-zookeeper.yml
spring:
config:
# 引入其他配置文件
import: application.yml
cloud:
zookeeper:
connect-string: localhost:2181
- 主启动类开启服务发现
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentApp {
public static void main(String[] args) {
SpringApplication.run(PaymentApp.class, args);
}
}
- 进入zookeeper中查看
docker exec -it zookeeper /bin/bash
cd bin
zkCli.sh
ls /services
- zookeeper服务节点为临时节点
五、consul
1.准备
- docker启动consul
- 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
- yml配置:
spring:
config:
# 引入其他配置文件
import: application.yml
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
六、Ribbon
1.背景
- 原本使用RestTemplate调用远程服务,使用轮询算法。Ribbon是客户端使用多种策略调用远程微服务的中间件,进行负载均衡
- Ribbon本地负载均衡 VS Nginx服务端负载均衡
- Nginx是在用户访问的第一层进行负载均衡,将用户请求均匀分散到多个服务中。进入服务后,Ribbon首先获取远端微服务接口,在本地进行分散请求,按照某个算法请求到远端微服务(轮询、随机等)。因此Nginx又叫集中式负载均衡,Ribbon叫进程内负载均衡。
2. 使用
- Ribbon不能和@ComponentScan放在一个包下面,及@SpringBootApplication主启动类的包下面(该注解内部包含@ComponentScan)
- 自定义规则
@Configuration
public class MyRule {
@Bean
public IRule myRule(){
// d定义为随机
return new RandomRule();
}
}
- 启动类开启开启Ribbon客户端
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD_PAYMENT_SERVICE", configuration = MyRule.class)
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class,args);
}
}
七、openfeign
1.准备
- 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 服务消费者启动类添加注解@EnableFeignClients
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class,args);
}
}
- 添加接口,注意请求接口的参数需要指定名字,比如@PathVariable(“id”
@Service
@FeignClient(name = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
@GetMapping("payment/{id}")
ResultVO feignGetPayment(@PathVariable("id") Integer id);
}
- controller调用该接口就行
@GetMapping("feign/payment/{id}")
public ResultVO feignGetPayment(@PathVariable Integer id){
ResultVO resultVO = paymentFeignService.feignGetPayment(id);
return resultVO;
}
2.openfeign超时控制
- 在配置文件中设置
八、Hystrix
背景
- 服务降级是什么
当服务达到某种状态时,返回给客户端一个响应,避免让客户端一直等待 - 什么时候会出发服务降级
- 程序运行异常
- 超时
- 服务熔断
- 线程池打满
- 服务熔断是什么
服务达到最大并发事,直接拒绝访问 - 服务限流
将服务的访问限制在一个访问内,有序进行
1.服务降级
一般服务降级放在消费端
服务端使用
- 当服务端请求压力变大,客户端请求会存在等待比较久,一直阻塞。或者服务端报错,都需要给消费端提供一个友情提示,又叫服务降低,就是有个兜底的方案
- 启动类加注解@EnableCircuitBreaker
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@EnableCircuitBreaker
public class PaymentApp {
public static void main(String[] args) {
SpringApplication.run(PaymentApp.class,args);
}
}
- 业务类使用hystrix提供降级方案
@HystrixCommand(fallbackMethod = "requestErrorHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")})
@Override
public String requestError(Integer id) {
try {
Thread.sleep(5000);
return Thread.currentThread().getName()+",error,"+id;
} catch (InterruptedException e) {
return "error";
}
}
public String requestErrorHandler(Integer id){
return Thread.currentThread().getName()+"系统繁忙,请稍后再试,"+id;
}
消费端使用
- 当消费者请求服务者,消费者也需要做一个超时的限制
- 启动类添加注解@EnableHystrix
@SpringBootApplication
@EnableEurekaClient
//@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MyRule.class)
@EnableFeignClients
@EnableHystrix
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class,args);
}
}
- 业务类处理
@HystrixCommand(fallbackMethod = "requestPaymentErrorHandler",commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")
})
@Override
public String requestPaymentError(Integer id) {
String response = restTemplate.getForObject(paymentHost + "/payment/hystrix/error/"+id, String.class);
return response;
}
public String requestPaymentErrorHandler(Integer id){
return "服务者超时";
}
全局降级策略
遇到的问题
- 每个方法都需要有一个降级措施,导致代码冗余
- 统一的降级措施和自定义的措施应该分开
- 降级策略和业务逻辑耦合较高
1.需要解决问题,就需要配置全局降级策略
- 类上加注解@DefaultProperties(defaultFallback = “globalErrorHandler” )
- 提供全局降级方案(空参方法)
public String globalErrorHandler(){
return "全局降级策略";
}
- 对需要进行全局降级处理的方法加注解
@HystrixCommand
@Override
public String requestError(Integer id) {
// Thread.sleep(5000);
int a = 10 / 0;
return Thread.currentThread().getName()+",error,"+id;
}
解决剪辑策略与业务逻辑耦合较高的问题
观察到一个问题,上面的降级策略与业务逻辑耦合在一起,需要进行解耦
- 需要使用OpenFeign进行微服务之间调用,需要开启OpenFeign对Hystrix的支持
feign:
hystrix:
enabled: true
- 需要将远程调用的微服务接口加入到OpenFeign的接口当中,并且@FeignClient的注解需要添加服务降级处理的类
@Service
@FeignClient(name = "CLOUD-PAYMENT-SERVICE",fallback = PaymentFeignServiceImpl.class)
public interface PaymentFeignService {
@GetMapping("payment/{id}")
ResultVO feignGetPayment(@PathVariable("id") Integer id);
@GetMapping("payment/hystrix/success/{id}")
String requestPaymentSuccess(@PathVariable("id") Integer id);
@GetMapping("payment/hystrix/error/{id}")
String requestPaymentError(@PathVariable("id") Integer id);
}
- 服务降级的类需要实现OpenFeign调用微服务的接口,并且在对应的方法中添加降级处理策略
@Service
@Slf4j
public class PaymentFeignServiceImpl implements PaymentFeignService {
@Override
public ResultVO feignGetPayment(Integer id) {
return null;
}
@Override
public String requestPaymentSuccess(Integer id) {
return "requestPaymentSuccess降级策略";
}
@Override
public String requestPaymentError(Integer id) {
return "requestPaymentError降级策略";
}
}
- 测试,当停掉远程服务端,消费端会进入降级处理
2.服务熔断
- 概念
类比保险丝,达到最大访问时,直接拒绝访问,出发服务降级策略,返回友好提示
熔断机制是应对雪崩效应的一种微服务链路保护机制,当某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
当检测到该节点微服务调用响应正常后(在间歇时间中不检查),恢复调用链路
- 步骤
- 在方法上加注解,开启熔断,制定降级策略
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallBack",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"), // 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"), // 在滑动窗口中收集请求次数,才会检查断路器状态
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 间歇时间。如果在这段时间内,发生断路的请求数超过了设置数则会熔断;如果发生了熔断,那么在这个时间后会成为半开状态
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"), // 错误百分比,到达百分比后会开启断路器
})
public String paymentCircuitBreaker(Integer id){
if (id < 0){
throw new RuntimeException("id 不能为负数");
}
return "调用成功";
}
public String paymentCircuitBreakerFallBack(Integer id){
return "id:" + id + "不能为负数,请稍后再试!";
}
- 当调用该方法时,如果id为负数,并且请求次数到熔断策略的要求,会进行服务熔断,直接进入降级策略
- 间歇时间中,请求一直是降级策略
- 间歇时间过后,断路器进入半开状态。会放开一个请求,如果服务请求正确,则回复主逻辑;如果服务请求错误,继续打开熔断器,休眠窗口重新计数。
3.HystrixDashboard
九、GateWay
背景
- 三大核心概念
- Route(路由):由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
- Predicate(断言):开发人员可以匹配HTTP请求中的所有内容(例如请求头和请求参数),如果请求与断言相匹配则进入路由
- Filter(过滤)指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改
- 为什么有Nginx了还要GateWay
- Nginx和GateWay都可以做负载均衡,但是GateWay能够根据服务信息,动态调整路由
- GateWay可以进行鉴权等功能
使用
- 首先添加gateway工程,注意pom依赖只需要三个
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<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-gateway</artifactId>
</dependency>
</dependencies>
- 在gateway的配置文件添加路由和断言,这里目前使用的uri是IP+PORT
spring:
application:
name: cloud-gateway
profiles:
active: eureka
cloud:
gateway:
routes:
- id: payment_9000 #名称唯一就行
uri: http://localhost:9000
predicates:
- Path=/payment/lb/**
- id: payment_9001
uri: http://localhost:9001
predicates:
- Path=/payment/*
- 实现动态路由
通过上面的配置,可以观察到uri被写死,如果使用微服务,这种路径是不可以的,因此需要实现动态路由。
- 使用微服务名进行路由,并且进行负载均衡
spring:
application:
name: cloud-gateway
profiles:
active: eureka
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_9000
uri: lb://cloud-payment-service # 提供服务的 微服务 地址
predicates:
- Path=/payment/lb/**
- id: payment_9001
uri: lb://cloud-payment-service
predicates:
- Path=/payment/*
- 常用的predicates
- 可以在官网查看使用方法
spring cloud predicate
- Filter
使用过滤器,在路由前或路由后进行的一些操作
spring cloud filter
- 生命周期
pre
post - 种类
GatewayFilter
GlobalFilter
尝试自定义过滤器:全局日志记录、统一网关鉴权
- 添加Filter类
@Service
@Slf4j
public class GatewayLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("********come in GatewayLogFilter"+new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if (uname == null){
log.info("**********用户名为null,非法参数!");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}