Spring Cloud
没有什么是加一层解决不了的,如果解决不了,那就再加一层
微服务:提倡单一的应用程序划分为一组小的服务,服务之间相互协调、配合。可以看作是Spring Boot
开发的一个又一个模块
Spring Cloud
:分布式微服务架构的一站式解决方案,多种微服务架构的集合体,俗称为微服务全家桶
热部署DevTools
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
添加配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
开启自动编译:
或者使用jrebel
多个服务调用
需要包装多个模块中的实体类相同
此时有两个服务在运行:
- 服务1为支付模块,提供添加支付、获取支付、删除支付的功能,主要包括
service
、controller
、bean
、mapper
包 - 服务2使用服务1中的服务,需要
bean
包、config
包、controller
包config
包中配置了RestTemplate
RestTemplate
提供了多种访问远程Http
服务的方法,是Spring
提供的用于访问rest
服务的客户端模板工具集,提供了边界访问restful
服务模板类
@Configuration
public class MyConfig {
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
服务1:
package com.xiaoxu.cloud.controller;
import com.xiaoxu.cloud.bean.Payment;
import com.xiaoxu.cloud.bean.Result;
import com.xiaoxu.cloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@Slf4j
@RequestMapping("/payment")
public class PaymentController {
private PaymentService service;
@PostMapping("/")
public Result<Integer> insert(@RequestBody Payment payment) {
int result = service.insertPayment(payment);
log.info("插入数据:{}, 结果:{}", payment, result);
if (result > 0) {
return new Result<>(200, "添加成功", result);
}
return new Result<>(400, "添加失败");
}
@GetMapping("/")
public Result<List<Payment>> getAllPayments() {
return new Result<>(400, "获取成功", service.getAllPayments());
}
@GetMapping("/{id}")
public Result<Payment> getPaymentById(@PathVariable Integer id) {
Payment payment = service.queryPaymentById(id);
if (payment == null) {
return new Result<>(400, "不存在");
}
return new Result<>(200, "获取成功", payment);
}
@DeleteMapping("/")
public Result<Integer> deletePaymentById(@RequestBody Payment payment) {
int i = service.deletePayment(payment);
if (i > 0) {
return new Result<>(200, "删除成功", i);
}
return new Result<>(400, "删除失败");
}
public PaymentController(PaymentService service) {
this.service = service;
}
}
服务2:
@RestController
@RequestMapping("/order")
public class OrderController {
// 必须带有http等协议,结尾必须有/
public static final String PAYMENT_URL = "http://localhost:8001/payment/";
private RestTemplate template;
@PostMapping("/payment")
public Result<Payment> insert(@RequestBody Payment payment) {
return template.postForObject(PAYMENT_URL, payment, Result.class);
}
@GetMapping("/payment")
public Result<List<Payment>> getAllPayment() {
return template.getForObject(PAYMENT_URL, Result.class);
}
public OrderController(RestTemplate template) {
this.template = template;
}
}
需要保证返回的数据结构相同,这个时候请求服务1和服务2有相同的效果
重构
可以看到两个服务中含有重复的bean
,可以新建一个模块,把重复的东西放到新的模块中
运行maven
的clean
,再运行install
即可将这个模块安装到本地库,删掉之前重复的类
<dependency>
<groupId>组名</groupId>
<artifactId>项目名</artifactId>
<version>版本</version>
<scope>compile</scope>
</dependency>
完成类型迁移后就直接可以使用了
Eureka
目前已停止更新。
读音yo͞oˈrēkə
服务治理:每个服务与服务之间的关系比较复杂,所以需要服务治理管理服务与服务之间的依赖关系,实现服务调用、负载均衡、容错、服务的发现与注册。
服务注册:采用客户端服务器的设计架构,作为服务注册功能的服务器,是服务注册中心
Eureka Server
提供服务、注册服务
Eureka Client
通过注册中心进行访问
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
yml:
eureka:
instance:
# 实例名称
hostname: localhost
client:
# false 代表不向注册中心自己注册自己
register-with-eureka: false
# false代表自己就是注册中心,职责是维护服务实例,不需要去检索服务
fetch-registry: false
service-url:
# 设置与Eureka Server交互的地址查询服务和注册服务都依赖这个地址
defaultZone: http://${eureka.instance.hostname}:${serve.port}/eureka
标注注册中心服务器:
@SpringBootApplication
@EnableEurekaServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud.version>2021.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
微服务接入Eureka
引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
添加配置:
eureka:
instance:
hostname: localhost
client:
# 为true代表入驻,否则不入驻
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
在启动类上添加@EnableEurekaClient
注解
服务注册:将服务信息注册到注册中心
服务发现:从注册中心获取服务的信息
实质:就是键值对,key
为服务名称,value
为服务的地址
Eureka集群
微服务RPC
远程服务的调用核心是高可用,实现负载均衡和故障容错
原理是互相注册,例如有两个集群:1
和2
,那么1
注册2
,2
注册1
。如果有三个集群,1
、2
、3
,那么1
注册2
、3
,2
注册1
、3
,3
注册1
、2
,也就是每个单机除了自己需要包含其他的服务单机
假设现在有两个集群,现在只需要做到,那么1
注册2
,2
注册1
:
-
server: port: 7002 spring: application: name: cloud-eureka-server7002 eureka: instance: hostname: server7002 client: register-with-eureka: false fetch-registry: false service-url: defaultZone: http://localhost:7001/eureka
-
server: port: 7001 eureka: instance: hostname: server7001 client: register-with-eureka: false fetch-registry: false service-url: defaultZone: http://localhost:7002/eureka spring: application: name: cloud-eureka-server7001
而客户端需要填写这两个集群的地址:
-
server: port: 8001 spring: application: name: cloud-payment-service datasource: mvc: hiddenmethod: filter: enabled: true eureka: instance: hostname: localhost client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
-
server: port: 80 spring: application: name: cloud-consumer-order80 eureka: instance: hostname: localhost client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
服务提供者的集群
这里的服务提供者是payment
,多个服务提供者集群的spring.applicatio.name
相同
多个服务的端口号不同,其他地方保持一致
在客户端中:
-
配置类
RestTemplate
返回实例的方法中添加负载均衡@LoadBalanced
注解-
@Configuration public class MyConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } }
-
-
修改原本的
url
为服务名称-
@RestController @RequestMapping("/order") public class OrderController { // 必须带有http等协议,结尾必须有/,修改为服务名称 public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE/payment/"; private RestTemplate template; @PostMapping("/payment") public Result<Payment> insert(@RequestBody Payment payment) { return template.postForObject(PAYMENT_URL, payment, Result.class); } @GetMapping("/payment") public Result<List<Payment>> getAllPayment() { return template.getForObject(PAYMENT_URL, Result.class); } public OrderController(RestTemplate template) { this.template = template; } }
-
这时候会交替使用不同的服务,也是轮询。
消费者这时候只关注服务名称,无需关注服务的IP
地址
balance中文为均衡、平衡、余额,读音bæləns
,loadBlance
为负载均衡
actuator信息完善
actuator中文为制动、传动,读音为ˈæktjuˌeɪtər
在eureka
服务页面显示的信息过于杂乱
没有这配置之前显示的内容大致如下:
Application | AMIs | Availability Zones | Status |
---|---|---|---|
CLOUD-CONSUMER-ORDER80 | n/a (1) | (1) | UP (1) - LAPTOP-BV21677A:cloud-consumer-order80:80 |
CLOUD-PAYMENT-SERVICE | n/a (2) | (2) | UP (2) - LAPTOP-BV21677A:cloud-payment-service:8002 ,LAPTOP-BV21677A:cloud-payment-service:8001 |
修改服务显示的内容、显示IP
:
eureka:
instance:
instance-id: 自定义名称
prefer-ip-address: true
服务发现Discovery
对外暴露这个服务的信息,仅能应用于EurekaClient
上不能应用于EurekaService
上
需要在主启动类上添加@EnableDiscoveryClient
,在需要的位置注入org.springframework.cloud.client.discovery.DiscoveryClient
即可
例如:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/status")
@Slf4j
public class StatusController {
@Autowired
DiscoveryClient discoveryClient;
@GetMapping("/service")
public List<String> getDiscoveryService() {
discoveryClient.getServices().forEach(it -> {
log.info("************{}", it);
});
return discoveryClient.getServices();
}
@GetMapping("/service/{name}")
public List<ServiceInstance> getInstance(@PathVariable String name) {
List<ServiceInstance> instances = discoveryClient.getInstances(name);
log.info("instance = {} ", instances);
return instances;
}
}
Eureka自我保护
可以在管理页面看到:
以下红字:
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
这就说明进入了保护模式
Eureka Server
将会尝试保护服务注册表中的信息,不在删除服务注册表中的数据,也就是不会注销任何微服务
也就是说:某一个时刻某个微服务不能用了,Eureka
不会立即清理,依旧会保存这个微服务的信息,默认将在90s
后进行注销这个微服务。在短时间内丢失大量的客户端时,Eureka
会任务可能发生了网络故障,那么将会进入自动保护模式。宁可保留错误的服务注册信息,也不会盲目的注销任何可能健康的服务实例
禁用自我保护
服务修改以下配置即可:
- 第一项代表是否关闭保护模式
- 第二项代表心跳超时时间(毫秒)
eureka:
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 200
客户端修改发送心跳的频率
修改以下配置:
- 第一项代表间隔秒数发送心跳
- 第二项代表收到最后一个心跳等待的时间上限
eureka:
instance:
lease-renewal-interval-in-seconds:秒数
lease-expiration-duration-in-seconds: 秒数
Consul
consul中文为领事,读音为ˈkɑːnsl
,是一套开源的分布式服务发现和配置管理系统,由HashiCrop使用GO开发
提供了服务治理、配置中心、控制总线等功能,可以根据需要使用这些功能,支持跨数据中心的WAN广域网集群,提供图形界面,跨平台,支持:
- 服务发现:提供HTTP和DNS两种方式
- 健康检测,支持多种方式
- KV存储:key-value
- 多数中心
- 可视化Web界面
下载安装:https://www.consul.io/downloads
启动:
consul agent -dev
启动后打开:http://localhost:8500
即可进入web管理页面
引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
在主启动类上添加@EnableDiscoveryClient
注解
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class Consul8006pplication {
public static void main(String[] args) {
SpringApplication.run(Consul8006pplication.class, args);
}
}
配置文件按照以下格式填写即可:
server:
port: 8006
spring:
application:
name: consul-provider-payment-8006
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
如果搭建服务器集群,也只需要保证名称相同即可
客户端的配置和服务器的配置基本相同,跟之前一样,也需要RestTemplate
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class MyConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
URL
依旧是需要填写服务名称标识
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/consumer")
public class MyController {
public static final String URL = "http://consul-provider-payment-8006/";
@Autowired
RestTemplate restTemplate;
@GetMapping("/")
public Map<String, String> load() {
return restTemplate.getForObject(URL + "payment/", HashMap.class);
}
}
Ribbon
中文为丝带、带子、飘带,读音为ˈribən
是NetFlix
提供的实现客户端负载均衡的工具
Nginx
是使用请求转发来实现的负载均衡(负载均衡 + RestTemplate
),Ribbon
是在调用时实现的负载均衡
可以和eureka
等结合起来
工作过程:
- 选择一个负载比较小的注册中心服务器
- 根据用户的策略,在注册中心的服务注册列表选择一个服务
- 提供了多种策略:轮询、随机、权重等策略
可以发现即使没有主动引入相关的负载均衡的依赖也能做到负载均衡,这是因为间接引入了org.springframework.cloud.loadbalancer
RestTemplate
get/postForObject
:返回对象为响应体中的数据转化的对象,可以理解为JSON
get/postForEntity
:返回ResponseEntity
对象,包含了响应的一些信息,例如响应头、状态码、响应体等
@GetMapping("/payment2")
public ResponseEntity<Result> getAllPayment2() {
ResponseEntity<Result> forEntity = template.getForEntity(PAYMENT_URL, Result.class);
HttpHeaders headers = forEntity.getHeaders();
log.info("headers = {}", headers);
log.info("body = {}", forEntity.getBody());
log.info("code = {}", forEntity.getStatusCode());
log.info("entity = {}", forEntity);
log.info("是否响应4XX:{}", forEntity.getStatusCode().is4xxClientError());
log.info("是否响应2XX:{}", forEntity.getStatusCode().is2xxSuccessful());
return forEntity;
}
2022-08-28 09:37:23.884 INFO 1620 --- [p-nio-80-exec-7] c.x.cloud.controller.OrderController : headers = [Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Sun, 28 Aug 2022 01:37:23 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]
2022-08-28 09:37:23.884 INFO 1620 --- [p-nio-80-exec-7] c.x.cloud.controller.OrderController : body = Result(code=400, message=获取成功8001, data=[{id=1, serial=aaaaaa}, {id=3, serial=0000000}])
2022-08-28 09:37:23.884 INFO 1620 --- [p-nio-80-exec-7] c.x.cloud.controller.OrderController : code = 200 OK
2022-08-28 09:37:23.884 INFO 1620 --- [p-nio-80-exec-7] c.x.cloud.controller.OrderController : entity = <200,Result(code=400, message=获取成功8001, data=[{id=1, serial=aaaaaa}, {id=3, serial=0000000}]),[Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Sun, 28 Aug 2022 01:37:23 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]>
2022-08-28 09:37:23.884 INFO 1620 --- [p-nio-80-exec-7] c.x.cloud.controller.OrderController : 是否响应4XX:false
2022-08-28 09:37:23.884 INFO 1620 --- [p-nio-80-exec-7] c.x.cloud.controller.OrderController : 是否响应2XX:true
自定义负载均衡的规则
自定义的配置类不能放在@ComponentScan
所扫描的当前包或者子包下,否则这个配置类将会被所有的客户端共享,得不到特殊化定制的目的,也就是放到不能够被SpringBoot
扫描到的地方,不能添加@Configuration
注解
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
public class LoadBalanceConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
在主启动类上指定所使用的规则
@SpringBootApplication
@EnableEurekaClient
@LoadBalancerClient(value = "需要使用的规则的服务标识", configuration = LoadBalanceConfig.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
原理:第n次请求 % 服务总数
以下为源码的信息,在第11-13行做的就是这个工作
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + serviceId);
}
return new EmptyResponse();
}
// Ignore the sign bit, this allows pos to loop sequentially from 0 to
// Integer.MAX_VALUE
int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
ServiceInstance instance = instances.get(pos % instances.size());
return new DefaultResponse(instance);
}
OpenFeign
feign中文为假装、伪装,读音为feɪn
用在客户端,也是进行服务调用,可以和以上组件结合起来实现负载均衡。使编写Http
客户端变得更容易,可以帮助我们定义和实现依赖服务接口的定义,只需要一个注解即可完成绑定
引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.1.3</version>
</dependency>
application.yml
保持不变,还是要有一个依赖的注册中心
使用步骤:
-
在主启动类上添加
@EnableFeignClients
-
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableFeignClients @EnableDiscoveryClient public class ConsumerOpenFeign80Application { public static void main(String[] args) { SpringApplication.run(ConsumerOpenFeign80Application.class, args); } }
-
-
自定义接口,添加
@Compoent
、@FeignClient("服务标识名称")
,在方法上提供与服务请求相同的路径(@XxxMapping("路径")
)、返回值、参数-
import com.xiaoxu.api.bean.Payment; import com.xiaoxu.api.bean.Result; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; import java.util.List; @Component @FeignClient("CLOUD-PAYMENT-SERVICE") public interface MyFeign { @GetMapping("/payment/") Result<List<Payment>> getAllPayments(); }
-
-
最后在需要的地方写自己的业务逻辑,然后注入以上接口
-
import com.xiaoxu.api.bean.Payment; import com.xiaoxu.api.bean.Result; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/feign") public class MyController { @Autowired private MyFeign myFeign; @GetMapping("/") public Result<List<Payment>> getAllPayments() { return myFeign.getAllPayments(); } }
-
Feign
自带有负载均衡的功能
如果有路径变量,必须要指定路径名称
@Component
@FeignClient("CLOUD-PAYMENT-SERVICE")
public interface MyFeign {
@GetMapping("/payment/timeout/{time}")
// 路径中指定名称,否则抛异常
String timeout(@PathVariable("time") Integer time);
}
超时控制
默认是连接超时为10s
,读取超时60s
配置:
feign:
client:
config:
default:
ConnectTimeOut: 5000
ReadTimeOut: 5000
日志
提供了日志打印功能,可以通过调整日志级别来了解Feign
中Http
请求细节
分为:
NONE
默认,不显示日志BASIC
仅显示请求方法、URL、响应状态码、执行时间HEADERS
除了BASIC之外,还有请求和响应的头FULL
除了HEADERS还有请求和响应的正文以及元数据
设置接口所在的日志等级为Debug
:
logging:
level:
com.xiaoxu.cloud.feign.controller.MyFeign: debug
方式1:
配置文件设置
feign:
client:
config:
default:
logger-level: full
方式2:
配置类
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
Hystrix
复杂的分布式体系结构的应用程序如果有非常多,如果其中一个失败,那么所有的依赖都会失败
Hystrix
是处理分布式系统的延迟和容错的开源库,能够保证在一个依赖出问题的情况下不会导致整体的服务失败,避免级联故障,当某个服务单元发生故障后,向调用方返回一个符合预期的、备选的响应,而不是长时间等待或者抛出无法处理的异常,保证服务调用方的线程不会长时间、不必要的占用,从而有效避免故障的蔓延或者雪崩,可以完成:
- 服务降级
- 服务熔断
- 接近的实时监控
- 限流
- 隔离等
已停更
- 服务降级(
fallback
)- 如果出现异常情况需要给出响应结果。例如给出“服务器忙”的提示
- 出现降级的情况:
- 程序运行异常
- 超时
- 服务熔断发生降级
- 线程池满了
- 服务熔断(
break
)- 如果服务达到最大的服务访问后,拒绝访问,调用服务降级的方法
- 服务限流(
flowlimit
)- 防止一窝蜂的请求,排队进行请求,有序进行
引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
服务端和消费端都可以使用
服务降级
使用注解:@HystrixCommand
可设置超时后调用后备的方法,只要服务不可用了或者抛出了异常就会立即进行服务降级
fallbackMethod
为兜底的方法
@HystrixCommand(fallbackMethod = "handlerMethod", commandProperties = {
@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "3000")
})
public String abnormal(@PathVariable Integer time) throws InterruptedException {
log.info("访问不正常的方法,时间延迟为:{}秒", time);
Thread.sleep(time * 1000);
log.info("执行完成");
return "延迟" + time + "秒";
}
public String handlerMethod(Integer time) {
return "出错了,你设置的超时时间是" + time + "秒";
}
还需要在主启动类上添加@EnableHystrix
注解
客户端和服务端都可以进行服务降级,但一般都是在服务端进行服务降级
全局服务降级
在类上添加@DefaultProperties(defaultFallback = "默认的处理方法")
注解,指定这个类的默认全局处理方法
在需要进行服务降级的方法上添加@HystrixCommand
,不需要填写任何的参数
如果指定了专门的处理方法,那么优先被专门的方法处理
@RestController
@RequestMapping("/hystrix")
@Slf4j
@DefaultProperties(defaultFallback = "defaultHandlerMethod")
public class MyController {
@HystrixCommand
@GetMapping("/normal")
public String routine() {
log.info("访问正常的方法");
if (new Random().nextInt(100) % 2 == 0) {
log.info("尝试抛出异常");
throw new RuntimeException();
}
return "正常方法";
}
@GetMapping("/abnormal/{time}")
@HystrixCommand(fallbackMethod = "handlerMethod", commandProperties = {
@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "3000")
})
public String abnormal(@PathVariable Integer time) throws InterruptedException {
log.info("访问不正常的方法,时间延迟为:{}秒", time);
Thread.sleep(time * 1000);
log.info("执行完成");
return "延迟" + time + "秒";
}
public String handlerMethod(Integer time) {
return "出错了,你设置的超时时间是" + time + "秒";
}
public String defaultHandlerMethod() {
return "这是默认的处理错误的兜底方法";
}
}
统配服务降级
在客户端进行降级时,可以很方便针对使用服务端的方法进行一一配置降级,先添加配置:
feign:
circuitbreaker:
enabled: true
统一进行修改hysrix
的超时时间:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 9000
使用自定义的类实现@FeignClient
注解标注的接口,并且自定义类中的返回值就为服务降级的结果,并指定fallback
为自定义类
@Service
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX8001", fallback = ApiHandler.class)
public interface Api {
@GetMapping("/hystrix/normal")
String normal();
@GetMapping("/hystrix/abnormal/{time}")
String abnormal(@PathVariable("time") Integer time);
}
import org.springframework.stereotype.Component;
@Component
public class ApiHandler implements Api{
@Override
public String normal() {
return "服务端的normal出现异常了-来自客户端的提示";
}
@Override
public String abnormal(Integer time) {
return "服务端的abnormal出问题了-来自客户端的提示";
}
}
如果服务端崩了,将会直接由服务降级的方法进行直接的处理,如果服务端也进行了降级的处理,优先使用服务端的结果
服务熔断
熔断后将会调用服务降级,是应对雪崩效应的微服务链路保护机制,当某个微服务出错或者响应时间过长时,将进行服务降级,熔断这个微服务的调用
默认是5秒内20次调用失败将会启动熔断,依旧 使用@HystrixCommand
注解
circuit中文为电路,读音为ˈsərkət
以下代码的含义是:10
秒内10
次请求有60%
是失败的就触发服务熔断
@GetMapping("/break/{num}")
@HystrixCommand(fallbackMethod = "breakHandler", commandProperties = {
@HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ENABLED, value = "true"), // 开启服务熔断
@HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "10"), // 请求次数
@HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS, value = "10000"), // 窗口期
@HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "60"), // 失败的百分比
})
public String serviceBreak(@PathVariable Integer num) {
if (num < 0) {
throw new RuntimeException();
}
return "已收到/hystrix/break/" + num + " 的请求";
}
public String breakHandler(Integer num) {
return "已被服务降级,num = " + num;
}
如果位于熔断的状态,时间窗口期内让其通过一次,如果成功了就自动改变为非熔断状态
Gateway
Spring Cloud
中的网关组件,底层使用netty
,位于Nginx
之后,作为微服务的入口
- 路由:是构建网关的基本模块,由一系列的断言和过滤器组成
- 断言:如果请求与断言相匹配则进行路由
- 过滤:使用过滤器可以将请求 被路由前或者路由后对请求进行修改
客户端向Gateway
发出请求,Gateway Handler Mapping
中找到与请求相匹配的路由后,将其发送到Web Handler
,在根据指定的过滤器链将请求发送到实际的业务逻辑
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
配置:
spring:
application:
cloud:
gateway:
routes:
- id: normal
uri: http://localhost:8001
predicates:
- Path=/hystrix/normal
- id: abnormal
uri: http://localhost:8001
predicates:
- Path=/hystrix/abnormal/**
配置路径和URL即可,uri
一定要写http://
添加网关后的效果类似于Nginx
的反向代理的效果,也可以起到隐藏端口的效果
也可以使用编码的方式配置:
-
@Configuration public class GatewayConfig { @Bean public RouteLocator consumerRouteLocator(RouteLocatorBuilder routeLocatorBuilder) { return routeLocatorBuilder.routes() .route("baidu", f -> f.path("/baidu") .uri("https://ip.skk.moe/")) .build(); } }
动态路由
可以发现上边的IP
地址是写死的,所以做到负载均衡,一次只能访问一个服务器,可以利用服务注册中心所提供的服务名实现动态路由
需要开启发现:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
修改uri
,由原来的uri: http://localhost:port
修改为uri: lb://服务名
,其他地方保持不变:
-
spring: cloud: gateway: discovery: locator: enabled: true routes: - id: normal uri: lb://CLOUD-PROVIDER-HYSTRIX8001 predicates: - Path=/hystrix/normal - id: abnormal uri: lb://CLOUD-PROVIDER-HYSTRIX8001 predicates: - Path=/hystrix/abnormal/** - id: break uri: lb://CLOUD-PROVIDER-HYSTRIX8001 predicates: - Path=/hystrix/break/**
-
lb
的意思是load balance
Predicate
中文为谓语、断言,读音为ˈpredɪkeɪt
查看启动日志,可以发现:
2022-09-05 09:43:36.600 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [After]
2022-09-05 09:43:36.600 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Before]
2022-09-05 09:43:36.600 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Between]
2022-09-05 09:43:36.600 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Cookie]
2022-09-05 09:43:36.600 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Header]
2022-09-05 09:43:36.600 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Host]
2022-09-05 09:43:36.600 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Method]
2022-09-05 09:43:36.600 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Path]
2022-09-05 09:43:36.600 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Query]
2022-09-05 09:43:36.600 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [ReadBody]
2022-09-05 09:43:36.601 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [RemoteAddr]
2022-09-05 09:43:36.601 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [XForwardedRemoteAddr]
2022-09-05 09:43:36.601 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Weight]
2022-09-05 09:43:36.601 INFO 39728 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [CloudFoundryRouteService]
在之前的使用中,只用到了path
规则:
-
在某个时间之后,也就是在这个时间之后这个规则才有效
-
时间的格式为
2022-09-05T09:52:24.300069800+08:00[Asia/Shanghai]
-
可以通过
System.out.println(ZonedDateTime.now());
获取这种格式的时间 -
以下代表在这个时间之后才能够正常的访问这个路径
-
routes: - id: normal uri: lb://CLOUD-PROVIDER-HYSTRIX8001 predicates: - Path=/hystrix/normal - After=2022-09-05T09:55:24.300069800+08:00[Asia/Shanghai]
-
-
在某个时间之前
- Before=2022-09-05T09:58:24.300069800+08:00[Asia/Shanghai]
-
在某个时间之间
- Between=2022-09-05T15:13:24.300069800+08:00[Asia/Shanghai],2022-09-05T15:14:24.300069800+08:00[Asia/Shanghai]
-
- Cookie: cookie名称,正则表达式匹配规则
-
- Header=请求头,值正则表达式
-
- Method=POST
-
Query=参数名,值的正则表达式
带有请求参数的规则
Filter
请求被路由前或者后进行路由
自定义全局过滤器:
-
自定义类实现
GlobalFilter
、Ordered
接口 -
import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Component @Slf4j public class MyLogGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { log.info("****************************自定义全局过滤器执行*******************************"); // 放行 return chain.filter(exchange); } @Override public int getOrder() { // 优先级,数字越小优先级越高 return 0; } }
分布式配置
现在项目中存在的问题:
- 每个项目都要有一个
yml
- 多个微服务都要访问数据库,都需要写同样的
yml
服务配置中心也是一个微服务
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
配置:
spring:
application:
name: cloud-config3344
cloud:
config:
server:
git:
uri: git仓库地址
search-paths: 搜索路径
访问方式:
/{label}/{}-{}.yml
http://localhost:6677/master/application-dev.yml
/{}-{}.yml
http://localhost:6677/application-dev.yml
,默认读取master
分支的内容
/{}/{}/{label}
http://localhost:6677/application/dev/master
,将会返回一个json
,可以自行解析
{label}
代表分支名称,例如master
│ application-dev.yml
│
├───config
│ application-dev.yml
│
└───my-config
test.yml
创建文件时需要遵守{}-{}的格式
客户端的配置
bootstrap.yml
的优先级大于application.yml
,所以使用bootstrap.yml
作为配置文件,但由于新版spring cloud
的修改,所以无法直接使用,需要引入:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!-- 针对 bootstrap.yml的配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
配置服务器:
spring:
application:
name: cloud-config-client3355
cloud:
config:
label: master
name: application
profile: dev
uri: http://localhost:6677/
可以尝试着注入到文件:
@RestController
@RequestMapping("/config")
public class MyController {
@Value("${name.message}")
private String message;
@RequestMapping("/message")
public String getMessage() {
return message;
}
}
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
在需要获取配置的类上添加@RefreshScope
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/config")
@RefreshScope
public class MyController {
@Value("${name.message}")
private String message;
@RequestMapping("/message")
public String getMessage() {
return message;
}
}
添加配置:
management:
endpoints:
web:
exposure:
include: "*"
更新配置后,手动对接受配置的客户端:http://ip:port/actuator/refresh
进行POST
请求,请求后即可进行更新
Bus 消息总线
是对Spring Cloud Config
的加强,主要完成配置的全自动刷新,支持RabbitMQ
和Kafka
消息代理,从之前的手动更新配置变成了消息中间件自动更新
总线:使用消息代理(消息队列)构建的共同消息主题,是所有微服务都连接上来。
所有的配置客户端都连接一个Topic
交换机,数据刷新时,同一个交换机中的服务都能收到通知,以用来更新配置
设计思想:
- 消息总线触发一个客户端的刷新,从而导致所有的客户端都刷新
- 消息总线触发服务端的刷新,从而刷新所有的客户端(更合适)
采用:消息总线触发一个客户端的刷新,从而导致所有的客户端都刷新的方式
配置服务端、客户端引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
如果配置文件发生改变了,对配置中心(配置服务器)的:http://ip:port/actuator/busrefresh
发送post
请求,这时候客户端的配置就更新了
这个时候配置中心的配置文件内容大致如下:
server:
port: 6677
spring:
application:
name: cloud-config3344
cloud:
config:
server:
git:
uri: https://配置文件地址
search-paths: my-config
rabbitmq:
addresses: localhost
port: 5672
username: guest
password: guest
eureka:
instance:
hostname: localhost
instance-id: cloud-config3344
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
management:
endpoints:
web:
exposure:
include: "bus-refresh"
两个配置客户端的配置文件内容大致如下:
server:
port: 3355
spring:
application:
name: cloud-config-client3355
cloud:
config:
label: master
name: application
profile: dev
uri: http://localhost:6677/
rabbitmq:
username: guest
password: guest
port: 5672
eureka:
instance:
hostname: localhost
instance-id: cloud-config-client3355
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
management:
endpoints:
web:
exposure:
include: "*"
配置中心的pom.xml
内容大致如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xiaoxu.config</groupId>
<artifactId>cloud-config-6677</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cloud-config-6677</name>
<description>cloud-config-6677</description>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2021.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
客户端的pom.xml
大致如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xiaoxu.config.client</groupId>
<artifactId>cloud-config-client3355</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cloud-config-client3355</name>
<description>cloud-config-client3355</description>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2021.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
打开RabbitMQ
的管理页面,可以看到创建了一个名为springCloudBus
的交换机,交换机的类型为topic
也可以定点通知,也就是只通知其中的一个,只需要请求时带上微服务的名称即可:POST请求配置中心的http://ip:port/actuator/busrefresh/微服务名称
Stream
消息驱动,不同的项目用到的消息队列可能是不同的,不同的消息队列是不可能直接进行通信的。Spring Cloud Stream
就是解决的这个问题,屏蔽底层的细节,直接使用spring cloud stream
就能无缝的操作多个消息队列,也就是统一消息的编程模型
官方文档给出的定义:spring cloud stream
是构建消息驱动的微服务框架
只需要通过所提供的Binder
对象与不同的消息中间件进行交互,目前支持整合RabbitMQ
、Kafka
、RocketMQ
Binder
连接中间件,屏蔽差异Channel
通道,对Queue
的抽象Source/Sink
消息的生产者和消费者
此时:有8801
生产者、8802
接收者
引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
生产者配置:
server:
port: 8801
spring:
application:
name: cloud-stream-producer8801
rabbitmq:
username: guest
password: guest
port: 5672
eureka:
instance:
hostname: localhost
instance-id: cloud-stream-producer8801
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
消费者和生产者配置:
server:
port: 8802
spring:
application:
name: cloud-stream-consumer8802
rabbitmq:
username: guest
password: guest
port: 5672
eureka:
instance:
hostname: localhost
instance-id: cloud-stream-consumer8802
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
生产者发送消息:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
@Component
public class MessageProvider {
@Autowired
private StreamBridge streamBridge;
public boolean sendMessage(String message) {
return streamBridge.send("cloud-in-0", MessageBuilder.withPayload(message).build());
}
}
消费者接收消息:
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.util.function.Consumer;
@Component
@Slf4j
public class ReceiveProvider {
@Bean
public Consumer<String> cloud() {
return message -> {
log.info("接收消息为:{}", message);
};
}
}
交换机的类型默认为topic
分布式请求链路跟踪 sleuth
监控多个程序之间的服务调用
下载:zipkin
访问http://127.0.0.1:9411/
可以进入web
管理面板
引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
<version>2.2.8.RELEASE</version>
</dependency>
配置:
spring:
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1
可以对多个微服务引入,如果这几个微服务是相互联系的,可以在网页上看到访问时调用的中间服务和延迟
Spring Cloud Alibaba
Spring Cloud Netflix
已全部都进入维护模式
https://spring-cloud-alibaba-group.github.io/github-pages/hoxton/zh-cn/index.html
Nacos
Nacos
全称为Naming Configuration Service
,就是配置中心+注册中心的组合,相当于Eureka + Config + Bus
官网:https://nacos.io/zh-cn/index.html
下载安装:https://github.com/alibaba/nacos/releases
在bin
目录下:.\startup.cmd -m standalone
进行启动
运行后,直接进入localhost:8848/nacos
,默认账号密码是nacos
支持CP
、AP
切换
服务注册
以下的配置针对于服务端
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
配置:
server:
port: 9001
spring:
application:
name: cloud-alibaba-provider-payment
cloud:
nacos:
discovery:
server-addr: localhost:8848
management:
endpoints:
web:
exposure:
include: '*'
和Eureka
一样,也可以多个服务实例使用一个名字,只需要保证spring.application.name
的值相同即可
负载均衡
以下的依赖、配置仅针对客户端
nacos
集成了ribbon
可以选择使用RestTemplate
,在配置类上一定要加@LoadBalanced
注解,否则将无法找到这个服务
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class MyConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@RestController
@RequestMapping("/consumer")
public class MyController {
@Autowired
private RestTemplate restTemplate;
public static final String URL = "http://cloud-alibaba-provider-payment";
@GetMapping("/nacos")
public String consumer() {
return restTemplate.getForObject(URL + "/my/nacos", String.class);
}
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class CloudConsumerPayment83Application {
public static void main(String[] args) {
SpringApplication.run(CloudConsumerPayment83Application.class, args);
}
}
如果想要使用openFeign
,需要引入spring-cloud-starter-loadbalancer
并且将nacos-discovery
集成的ribbon
移除掉,否则会导致spring-cloud-starter-loadbalancer
失效,移除ribbon
不会影响RestTemplate
的效果
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 移除ribbon -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-loadbalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
@Component
@FeignClient("cloud-alibaba-provider-payment")
public interface Api {
@GetMapping("/my/nacos")
String useApi();
}
@RestController
@RequestMapping("/consumer")
public class MyController {
@Autowired
private Api api;
@GetMapping("/feign/nacos")
public String feign() {
return api.useApi() + "feign";
}
}
配置中心
默认是支持自动刷新的
在 Nacos Spring Cloud 中,dataId
的完整格式如下:
${prefix}-${spring.profiles.active}.${file-extension}
prefix
默认为spring.application.name
的值,也可以通过配置项spring.cloud.nacos.config.prefix
来配置。spring.profiles.active
即为当前环境对应的 profile,详情可以参考 Spring Boot文档。 注意:当spring.profiles.active
为空时,对应的连接符-
也将不存在,dataId 的拼接格式变成${prefix}.${file-extension}
file-exetension
为配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file-extension
来配置。目前只支持properties
和yaml
类型。
例如在以下配置文件中:
server:
port: 9001
spring:
profiles:
active: dev
application:
name: cloud-alibaba-nacos-config
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
按照以上的规则,需要在nacos
创建的配置名为cloud-alibaba-nacos-config-dev.yml
引入依赖:
<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-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/config")
@RefreshScope
public class MyController {
@Value("${config.info}")
private String config;
@GetMapping("/info")
public String getConfigInfo() {
return config;
}
}
需要添加@RefreshScope
注解实现配置文件更新时自动刷新配置
命名分组
分为namespace + group + data Id
,类似于包名和类名:
namespace
用于区分部署环境,实现隔离- 不同的
namespace
属于不同的环境
- 不同的
Group
用于在逻辑上区分两个目标对象- 将不同的微服务划分到同一个分组中
data id
用于在逻辑上区分两个目标对象- 就是配置文件的名称
默认情况:
namespace
为public
group
为DEFAULT_GOUP
cluster
为default
spring:
profiles:
active: test
application:
name: cloud-alibaba-nacos-config
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
group: FIRST_GROUP
namespace: BEIJING_SERVER
namespace
为名称空间的id
Nacos持久化
默认nacos
使用的嵌入式的数据库,如果要搭建集群,那么需要使用MySQL
来保证各个集群的数据统一
创建一个数据库,执行nacos/conf/nacos-mysql.sql
,将会自动创建用到的表
修改nacos/conf/application.properties
,根据注释信息,修改如下内容:
#*************** Config Module Related Configurations ***************#
### If use MySQL as datasource:
# spring.datasource.platform=mysql
spring.datasource.platform=mysql
### Count of DB:
# db.num=1
db.num=1
### Connect URL of DB:
# db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
# db.user.0=nacos
# db.password.0=nacos
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=用户名
db.password.0=密码
重新运行nacos
即可
集群
将nacos/conf/cluster.conf.example
文件复制修改问cluster.conf
,在里边填入IP:端口号
即可,一行一个,IP
不能为127.0.0.1
或者localhost
,可以填入局域网IP
这里以搭建3
个集群为例:
例如:
10.54.70.146:8850
10.54.70.146:8860
10.54.70.146:8870
注意:Nacos2.0版本相比1.X新增了gRPC的通信方式,因此需要增加2个端口。新增端口是在配置的主端口(server.port)基础上,进行一定偏移量自动生成。
所以三个端口的距离尽量增大。
集群 Nacos端口 gRPC端口1 gRPC端口2 1 x.x.x.x:8850
x.x.x.x:9850
x.x.x.x:9851
2 x.x.x.x:8860
x.x.x.x:9860
x.x.x.x6:9852
3 x.x.x.x:8870
x.x.x.x:9870
x.x.x.x:9853
在nacos/conf/applicaton.properties
文件中修改每个集群的端口,因为这个时候创建了cluster.conf
,所以在启动nacos
时只需要使用以下命令,无需添加其他的参数:
.\startup.cmd
使用Nginx
进行反向代理时,除了对Nacos
端口进行负载均衡的配置之外,还需要对gRPC
进行单独配置
stream {
# tcp负载均衡
upstream nacos-tcp-2111 {
server 127.0.0.1:9850 weight=1;
server 127.0.0.1:9860 weight=1;
server 127.0.0.1:9870 weight=1;
}
# Nacos客户端gRPC请求服务端端口
server {
listen 2111;
proxy_pass nacos-tcp-2111;
}
# tcp负载均衡
upstream nacos-tcp-2112 {
server 127.0.0.1:9851 weight=1;
server 127.0.0.1:9861 weight=1;
server 127.0.0.1:9871 weight=1;
}
# Nacos服务端gRPC请求服务端端口
server {
listen 2112;
proxy_pass nacos-tcp-2112;
}
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
#gzip on;
upstream nacos {
server localhost:8850;
server localhost:8860;
server localhost:8870;
}
server {
listen 1111;
server_name localhost;
location / {
proxy_pass http://nacos/;
# root html;
# index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
官方文档上有一句:使用VIP/nginx
请求时,需要配置成TCP
转发,不能配置http2
转发,否则连接会被nginx
断开。
配置的客户端的地址尽量不要填localhost
之类的本地地址,端口号填写反向代理的nacos
的端口:
spring:
profiles:
active: test
application:
name: cloud-alibaba-nacos-config
cloud:
nacos:
discovery:
server-addr: x.x.x.x:1111
config:
server-addr: x.x.x.x:1111
file-extension: yml
group: FIRST_GROUP
namespace: BEIJING_SERVER
Sentinel
中文为哨兵,读音为ˈsent(ə)nəl
,用于服务熔断、服务降级,可以在web
页面进行管理
下载:https://github.com/alibaba/Sentinel/releases
选择带有deshboard
字样的版本即可,运行后打开localhost:8080
,账号密码都是sentinel
,可以直接进入web
管理页面
引入依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.csp/sentinel-datasource-nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
也是需要nacos
运行并引入
配置:
server:
port: 8401
spring:
application:
name: cloud-alibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
8719
端口是指定与dashboard
进行交互的端口,如果这个端口被占用了,默认将会不断的+1
,直到找到一个可用的端口
此时打开8080
的监控页,可能无法看到任何的监控信息,这是因为采用的懒加载的机制,等到第一次访问后,将会在监控页面看到详情信息
流量监控规则
在簇点链路可以对访问过的路径进行管理,也可以在流控规则中手动添加相关路径进行设置,其中:
- 资源名为路径
- 针对来源默认为
default
,也就是不区分来源,也可以填写微服务名,达到对某个微服务进行限制 - 阈值类型
QPS
:每秒的请求量
- 流控模式
- 直接:api达到限流条件时直接进行限流
- 关联:当关联的资源达到阈值时,限流自己
- 链路:只记录链路上的流量,也就是指定资源从入口资源进来的流量,如果达到阈值就进行限流,在api级别上针对来源
- 流控效果
- 快速失败:直接失败,直接抛异常
- warm up:针对codeFactor(冷加载因子)的值进行
- 排队等待:匀速排队,使请求匀速通过,必须设置
QPS
,否则没有效果
流控模式-直接:当超过设置的每秒请求的次数之后,请求的响应会显示为:Blocked by Sentinel (flow limiting)
流控模式-关联:当A
关联的资源B
达到阈值后,限制A
自己,比如支付服务接口达到阈值了,对订单服务进行限制
流控效果-warm up
(预热):
- 服务有可能出现长时间没有访问,然后一下子来了许多个访问,如果突然访问量增加,服务器可能顶不住
- 例如单机阈值为
10
,时长为5
,使用预热模式,将会在5
秒内慢慢的提高单机阈值到10
- 将会从
阈值/3
开始,例如阈值为10
,将会从3
作为第一次的阈值
流控效果-排队等待:只能在阈值类型是QPS
的情况下使用
- 适用于处理间隔性的突发流量
熔断降级
策略:
- 慢调用比例 (
SLOW_REQUEST_RATIO
):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。 - 异常比例 (
ERROR_RATIO
):当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是[0.0, 1.0]
,代表 0% - 100%。 - 异常数 (
ERROR_COUNT
):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
降级后,在时间窗口内对降级的资源所有的调用都自动熔断
热点key
热点是经常访问的数据,可以对访问频率前k
的数据进行限制
@GetMapping("/hotkey")
@SentinelResource(value = "名称", blockHandler = "兜底方法名称")
public String hotKey(@RequestParam(value = "content", required = false) String content,
@RequestParam(value = "param", required = false) String param) {
return "hotkey - content = " + content + ", param = " + param;
}
public String 兜底方法(String content, String param
, BlockException exception) {
return "hotkey - content = " + content + ", param = " + param + ":(";
}
在热点规则中,资源名为@SentinelResource
中自定义的名称,参数索引从0
开始,针对以上的代码:
索引 | 参数 |
---|---|
0 |
content |
1 |
param |
如果不在@SentinelResource
中指定处理异常的兜底方法,那么将会直接把异常抛到前端页面
也可以设置例外情况,也就是某个参数达到特殊值后的限制和平常不一样,例如当param
参数的值为"300"
时,限制QPS
为100
,在其他情况限制1
- 参数的类型要和实际处理这个请求的方法一致,设置后需要点添加按钮
- 当代码中有异常抛出时,将不会经过兜底方法
系统规则
从应用入口进行流量限制,经合多方面的配置等因素进行限流,在系统规则页进行新增
- Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的
maxQps * minRt
计算得出。设定参考值一般是CPU cores * 2.5
。 - CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0)。
- RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
Sentinel Resouce
可以按照资源名限制
使用方法类似于:
@GetMapping("/resource")
@SentinelResource(value = "resource", blockHandler = "resourceHandler")
public Map<String, String> resource() {
HashMap<String, String> map = new HashMap<>();
map.put("code", "200");
map.put("status", "访问成功");
return map;
}
public Map<String, String> resourceHandler(BlockException exception) {
HashMap<String, String> map = new HashMap<>();
map.put("code", "400");
map.put("status", "访问失败");
map.put("cause", exception.getMessage());
return map;
}
单独写一个兜底的方法,并在注解中指定兜底的方法,请求的方法中如果含有参数(例如路径变量、参数对应的变量),可以在兜底方法的参数列表中填写这些参数,还可以添加BlockException
类型的参数,用来表示默认抛出的一个异常
@SentinelResource
注解不支持private
方法
以上方案也存在着一些问题:
- 对于每一个方法,都需要提供一个兜底的方法,否则发生限流/熔断降级时将会使用默认的兜底方法,也就是屏幕上输出
Blocked by Sentinel (flow limiting)
- 兜底的方法和处理请求的方法耦合在一块了
- 没有体现出全局统一的处理方法
自定义限流处理类
自定义一个类,类中所有的方法都是静态的方法,静态方法上必须要添加BlockException
类型的参数,否则将不会执行兜底方法,并且前端页面将会报500错误,在需要有兜底方法的请求方法上:
@SentinelResource(value = "名称", blockHandlerClass = 自定义兜底类.class, blockHandler = "自定义的兜底方法")
package com.xiaoxu.cloud.handler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import java.util.HashMap;
import java.util.Map;
public class CustomerBlockHandler {
public static Map<String, Object> firstHandler(BlockException e) {
Map<String, Object> map = new HashMap<>();
map.put("code", "400");
map.put("cause", e);
map.put("message", "失败---first");
return map;
}
public static Map<String, Object> secondHandler(BlockException e) {
Map<String, Object> map = new HashMap<>();
map.put("code", "400");
map.put("cause", e);
map.put("message", "失败---second");
return map;
}
}
@GetMapping("/first")
@SentinelResource(value = "first", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "firstHandler")
public Map<String, Object> first() {
HashMap<String, Object> map = new HashMap<>();
map.put("code", "200");
map.put("status", "访问成功---first");
return map;
}
@GetMapping("/second")
@SentinelResource(value = "second", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "secondHandler")
public Map<String, Object> second() {
HashMap<String, Object> map = new HashMap<>();
map.put("code", "200");
map.put("status", "访问成功---second");
return map;
}
服务熔断
服务熔断就是在出现异常情况时进行熔断
fallback
中文为倒退、退却、让出
可以设置异常的兜底方法:
@SentinelResource(value = "index", blockHandlerClass = MyBlockHandler.class, blockHandler = "itemHandler",
fallback = "itemExceptionFallback")
public String getItem(@PathVariable Integer index) {
return list.get(index) + " - 服务器2";
}
public String itemExceptionFallback(Integer index, Throwable e) {
return "抛异常了,服务器2,index = " + index + ", 异常为:" + e;
}
兜底方法中可以有请求方法中的参数,可以根据需要添加@PathVariable
等注解,也可以不添加,但兜底方法中的异常接受参数不能使用Exception
接受,需要使用Throwable
进行接收
也可以将异常处理方法单独放到一个类中,使用@SentinelResource(fallbackClass = 类名.class)
指定异常处理类:
public class MyExceptionFallback {
public static String itemExceptionFallback(Integer index, Throwable e) {
return "抛异常了,服务器1,index = " + index + ", 异常信息为:" + e;
}
}
@GetMapping("/{index}")
@SentinelResource(value = "index", blockHandlerClass = MyBlockHandler.class, blockHandler = "itemHandler",
fallbackClass = MyExceptionFallback.class, fallback = "itemExceptionFallback")
public String getItem(@PathVariable Integer index) {
return list.get(index) + " - 服务器1";
}
兜底方法也可以忽略异常:
@SentinelResource(exceptionsToIgnore = {java.lang.IndexOutOfBoundsException.class})
如果忽略后发生了这个异常,将不会执行兜底方法
持久化
每次重启服务,sentinel
的规则都会丢失,所以需要持久化,可以将规则直接保存到Nacos
中
需要:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
引入配置:
spring:
application:
name: cloud-alibaba-payment
profiles:
active: dev
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
sentinel:
transport:
dashboard: localhost:8080
port: 8719
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloud-alibaba-sentinel-payment
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
在nacos
创建一个名为指定的dataId
格式为json
的配置文件,文件内容格式如下:
[
{
"resource": "index",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
resource
:资源名称limitApp
:来源应用grade
:阈值类型,0
表示线程数,1
表示QPS
count
:单机阈值strategy
:流控模式,0
表示直接,1
表示关联,2
表示链路controlBehavior
:流控效果,0
表示快速失败,1
表示Warm Up
,2
表示排队等待clusterMode
:是否集群
Seata 分布式事务
在微服务的架构中,可能存在着多库多表,需要保证多个服务对数据库访问时的一致性
Transcation ID XID
全局唯一的事务ID
Transaction Coordinator
事务协调器,维护全局的事务的运行状态,负责协调、驱动全局事务的提交或者回滚Transaction Manager
控制事务的边界,负责开启全局事务,发起全局提交或者全局回滚Resource Manager
控制分支事务,负责分支注册、状态的汇报,接收事务协调器的指令
TM
向TC
申请开启一个全局事务,全局事务创建成功后生成一个全局唯一的XID
XID
在微服务调用链的上下文传播RM
向TC
注册分支事务,将其踏入XID
全局事务的管辖TM
向TC
发起针对XID
全局提交或者回滚的决议TC
调度XID
下管辖的所有分支事务完成提交或者回滚
安装
下载:https://github.com/seata/seata/releases
修改配置文件可参阅:https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html
服务注册中心、配置中心使用nacos
,数据库使用MySQL
:
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: seata
password: seata
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: localhost:8848
namespace: public
group: SEATA_GROUP
data-id: seataServer.properties
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos:
application: seata-server
server-addr: localhost:8848
group: SEATA_GROUP
namespace: public
cluster: default
store:
# support: file 、 db 、 redis
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true
user: 账号
password: 密码
min-conn: 5
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 100
max-wait: 5000
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
需要创建一个数据库,并执行seata\script\server\db
下相应数据库.sql
根据文档可知,对jdk17
的支持不是特别好,直接运行将会报错,需要修改启动命令:
%JAVACMD% %JAVA_OPTS% %SKYWALKING_OPTS% --add-opens java.base/java.lang=ALL-UNNAMED -server -Dloader.path=../lib -Xmx2048m -Xms2048m -Xmn1024m -Xss512k -XX:SurvivorRatio=10 -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=1024m -XX:-OmitStackTraceInFastThrow -XX:-UseAdaptiveSizePolicy -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath="%BASEDIR%"/logs/java_heapdump.hprof -XX:+DisableExplicitGC -Xlog:gc:"%BASEDIR%"/logs/seata_gc.log -verbose:gc -Dio.netty.leakDetectionLevel=advanced -classpath %CLASSPATH% -Dapp.name="seata-server" -Dapp.repo="%REPO%" -Dapp.home="%BASEDIR%" -Dbasedir="%BASEDIR%" -Dspring.config.location="%BASEDIR%"/conf/application.yml -Dlogging.config="%BASEDIR%"/conf/logback-spring.xml -jar "%BASEDIR%"/target/seata-server.jar %CMD_LINE_ARGS%
注意:Nacos
要以单个的形式启动,即.\startup.cmd -m standalone
订单服务、库存服务、账户服务,用户下单在微服务创建订单,调用远程库存服务扣减库存,通过账户服务扣余额,最后在订单服务标记 订单状态
下订单->减库存->扣余额->改变订单状态
以下依赖中,第二个依赖不兼容jdk17
,添加此依赖无法启动项目,如果不添加此依赖能够正常启动项目,但会造成使用的openfeign
全局事务标识符xid
无法传递,可以尝试手动传递xid
,例如在请求头中放入xid
,使用RootContext.bind(xid)
进行手动绑定
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>2.2.0.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
配置:
seata:
tx-service-group: default-tx-group # 需要保证与第19行内容一致
enabled: true
enable-auto-data-source-proxy: true
config:
type: nacos
nacos:
namespace: aa34dcea-fa3e-43a0-a0a9-5567ddd5234b # 需要手动创建一个命名空间,不能使用public
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: public
service:
vgroupMapping:
default-tx-group: default # 键需要保证与第2的值一致
在nacos
中创建一个名为service.vgroupMapping.20行内容
的配置文件,文件类型为txt
,内容为20行
的值
完整配置文件类似于:
server:
port: 2001
spring:
application:
name: cloud-alibaba-seata-order-service
datasource:
url: jdbc:mysql://localhost:3306/xxxx
username: xxxx
password: xxxx
cloud:
nacos:
discovery:
server-addr: localhost:8848
mvc:
hiddenmethod:
filter:
enabled: true
management:
endpoints:
web:
exposure:
include: '*'
seata:
tx-service-group: default-tx-group
enabled: true
enable-auto-data-source-proxy: true
config:
type: nacos
nacos:
namespace: aa34dcea-fa3e-43a0-a0a9-5567ddd5234b
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: public
service:
vgroupMapping:
default-tx-group: default
在需要全局事务的地方使用@GlobalTransactional
注解,这个注解可以指定忽略的异常等信息
Q.E.D.