三、SpringCloud
一、分布式系统的组成
- 微服务:
SpringBoot
- 注册中心/配置中心:
Spring Cloud Alibaba Nacos
- 网关:
Spring Cloud Gateway
- 远程调用:
Spring Cloud OpenFeign
- 服务熔断:
Spring Cloud Alibaba Sentinel
- 分布式事务:
Spring Cloud Alibaba Seata
二、获取所有服务列表
@Resource
DiscoveryClient discoveryClient;
@Test
void discoveryClientTest(){
// 获取所有服务的名字
List<String> services = discoveryClient.getServices();
services.forEach(System.out::println);
// 获取服务的实例(参数是服务名字)
services.forEach((name->{
List<ServiceInstance> instances = discoveryClient.getInstances(name);
instances.forEach(i -> {
String instanceId = i.getInstanceId();
String host = i.getHost();
int port = i.getPort();
String serviceId = i.getServiceId();
URI uri = i.getUri();
String scheme = i.getScheme();
System.out.println("实例id:"+instanceId+
"\n主机:"+host+
"\n端口:"+port+
"\n服务id:"+serviceId+
"\nuri:"+uri+
"\nscheme:"+scheme);
System.out.println();
});
}));
}
三、调用远程服务的三种方式
第一种:直接调用,自己选择服务器
// 基础:没有负载均衡的获取远程服务
private Product getProductRemote(Long productId) {
// 获取远程的服务器列表
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
// 获取第一个服务器实例
ServiceInstance instance = instances.get(0);
// 构建请求URL
String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/product/" + productId;
log.info("远程请求URL:{}", url);
// 发送请求
return restTemplate.getForObject(url, Product.class);
}
第二种:使用LoadBalancerClient
// 进阶2:完成负载均衡的获取远程服务
private Product getProductRemoteWithLoadBalance(Long productId) {
// 获取远程的服务器列表 -- (负载均衡的获取)
ServiceInstance instance = loadBalancerClient.choose("service-product");
// 构建请求URL
String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/product/" + productId;
log.info("远程请求URL:{}", url);
// 发送请求
return restTemplate.getForObject(url, Product.class);
}
第三种:使用@LoadBalanced
// 进阶3:直接在 config 中 OrderServiceConfig 类,创建restTemplate对象时,指定使用负载均衡,那末,在调用的时候直接就会
// 使用负载均衡的方式来获取远程服务
private Product getProductRemoteWithLoadBalanceAnnotation(Long productId) {
// 构建请求URL,直接写微服务的名字就好了,不用写具体的地址和端口,
// 因为在 config 中 OrderServiceConfig 类,创建restTemplate对象时,指定使用负载均衡,
// service-product 会被动态替换为负载均衡器的地址和端口
String url = "http://service-product/product/" + productId;
log.info("远程请求URL:{}", url);
// 发送请求
return restTemplate.getForObject(url, Product.class);
}
四、注册中心宕机情况
注册中心宕机,分为两种情况:
第一种:如果注册中心是在服务注册后且执行了远程调用后宕机的,那末,由于远程调用过,因此会存在缓存,不会每次都请求注册中心获取最新的服务
列表而是使用缓存中的服务列表,只有当注册中心更新时,才会从注册中心重新获取的服务列表。如果注册中心宕机,那末,也不影响从缓存中获取请求地址第二种:如果注册中心是在服务注册前或调用前就宕机的,那末,由于注册中心还没启动,因此第一次远程调用会请求注册中心,而此时的注册中心处于
宕机状态,因此会报错。
五、配置中心
1、配置中心的依赖
<!--配置中心的依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
2、导入配置中心的配置文件
在导入配置中心的依赖后,在微服务的配置文件中导入配置文件
spring:
config:
import: nacos:service-order.yml # 从 nacos 的配置中心导入配置文件
3、在配置中心创建配置文件
注意:配置文件的
Data ID
就是导入时指定的文件名: service-order.yml(注意有后缀)
4、添加自动刷新的注解,启用自动刷新配置文件,当配置文件更新时
@RefreshScope // 表示对于nacos配置中心的配置文件的变化,会重新加载配置文件(自动刷新更新配置的值)
@RestController
public class OrderController {}
5、不检查配置文件的引入
如果引入了配置中心的依赖,但是没有在配置中导入配置文件,那末,在启动时会报错,因此需要在配置文件中添加以下配置,
来禁用检查配置文件的引入。
配置的声明规则,先导入优先,后声明优先,先用项目中的配置,再用配置中心的配置,因此如果配置相同
则优先使用项目中的配置。
报错信息:
Connected to the target VM, address: '127.0.0.1:62555', transport: 'socket'
16:49:36.515 [main] ERROR org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter --
***************************
APPLICATION FAILED TO START
***************************
Description:
No spring.config.import property has been defined
Action:
Add a spring.config.import=nacos: property to your configuration.
If configuration is not required add spring.config.import=optional:nacos: instead.
To disable this check, set spring.cloud.nacos.config.import-check.enabled=false.
Disconnected from the target VM, address: '127.0.0.1:62555', transport: 'socket'
解决办法:增加一个配置信息,禁用检查配置文件的引入
spring.cloud.nacos.config.import-check.enabled=false
6、配置中心的数据隔离
六、OpenFeign 声明式远程调用
1、依赖引入
注意:前提要引入 loadbalancer 的依赖,因为是默认的负载均衡。
<!--OpenFeign的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2、开启OpenFeign的功能
@SpringBootApplication
@EnableFeignClients // 开启OpenFeign的功能
public class ServiceOrderApplication {}
3、编写远程调用的接口
@FeignClient(value = "service-product") // 指定远程调用的服务的名字
public interface ProductClient {
@GetMapping("/product/{productId}") // 指定远程调用的服务的路径
Product getProduct(@PathVariable("productId") Long productId); // 指定远程调用的服务的方法
@GetMapping("/productWhitToken/{productId}") // 指定远程调用的服务的路径
Product getProduct(@PathVariable("productId") Long productId, @RequestHeader("Authorization") String token);
}
4、调用远程服务
// 注入远程调用的接口,直接调用即可
@Resource
ProductClient productClient;
补充:不仅可以调用注册中心的微服务,还可以使用第三方的AIP,只需要指明
url 路径即可。例如:// 通过 url 路径调用第三方的API @FeignClient(value = "remote-service",url = "http://www.baidu.com") public interface ProductClient { @GetMapping("/{wd}") // 指定远程调用的服务的路径 Product getProduct(@PathVariable("wd") String word); // 指定远程调用的服务的方法 }
5、Feign 的日志配置
- 在配置文件中指定远程调用所在的类 或 包。
logging: level: dz.cn.remote: debug
- 在配置类中,引入 bean 对象,指定日志的级别
@Configuration public class FeignConfig { @Bean Logger.Level feignLoggerLevel(){ return Logger.Level.FULL; } }
6、Feign 的超时控制
当某个链路中的一个远程调用的服务出现卡顿时,可能导致整个链路都出现卡顿,如果这个时候
有大量的请求同时访问,那末可能导致整个服务器的资源耗尽,导致服务雪崩。
- 连接超时(
connectTimeout
):默认连接超时 10 秒,默认返回读取信息 - 读超时(
readTimeout
):读取超时 60 秒
spring:
cloud:
openfeign:
client:
config:
default: # 默认配置,如果没有指定,直接使用默认配置
connect-timeout: 1000
read-timeout: 3000
service-product: # 单独配置
connect-timeout: 3000 # 连接超时时间
read-timeout: 5000 # 读取超时时间
7、Feign 的重试机制
远程调用失败后,还可以进行多次尝试,如果某次成功,则返回ok,否则依然返回错误。
需要向Spring中注入重试机制的配置类。
// 配置重试次数,也可以传参数:间隔时间,最大间隔时间,重试次数,
// 注意每次的间隔时间是递增的,默认是 100 毫秒,下一次请求为上一次间隔时间的 1.5 倍
// 但是不超过指定的最大间隔时间
@Bean
Retryer retryer(){
return new Retryer.Default(); // 默认值为:this(100L, TimeUnit.SECONDS.toMillis(1L), 5);
}
8、Feign 的请求拦截器
在远程调用服务之前,可以对请求内容进行拦截修改,例如:添加请求头,添加请求参数等。
@Component // 将该类注入到Spring容器中,这样就会自动扫描,不需要在配置文件中配置
public class XTokenRequestInterceptor implements RequestInterceptor {
/**
* 请求拦截器
* @param requestTemplate 请求模板(请求信息)
*/
@Override
public void apply(RequestTemplate requestTemplate) {
System.out.println("拦截器调用");
// 添加请求头
requestTemplate.header("X-Token", UUID.randomUUID().toString());
// 添加请求体
// requestTemplate.body(byte[] data, Charset charset);
// 添加查询信息
requestTemplate.query("hh", "hhh");
}
}
9、Feign 的Fallback 兜底返回
当远程调用失败时,可以进行兜底返回,例如:返回一个默认的响应信息,或者返回一个错误码。
注意:兜底返回需要一个熔断配置,需要引入熔断的依赖,以及开启熔断功能。
# 开启熔断机制 feign: sentinel: enabled: true
// 1、创建一个类,实现远程调用的接口,并实现其中的方法。
@Component
public class ProductFeignClientFallback implements ProductFeignClient {
@Override
public Product getProductById(Long id, String token) {
Product product = new Product();
product.setId(id);
product.setProductName("iPhone 15-" + id);
product.setPrice(new BigDecimal("12.5"));
product.setNum((int) (id + 10));
return product;
}
}
// 2、在远程调用接口中,指明兜底回调类 fallback = ProductFeignClientFallback.class
@FeignClient(value = "service-product",fallback = ProductFeignClientFallback.class)
public interface ProductFeignClient {
@GetMapping("/product/{id}")
Product getProductById(@PathVariable("id") Long id,@RequestHeader("Authorization") String token);
}
七、Sentinel 流量控制
1、Sentinel 的基本工作原理
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Spring Cloud Alibaba Sentinel 以流量为切入点,从流量控制
流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。
- 定义资源:
- 主流框架自动适配(web Servlet、Dubbo、SpringCloud)所有 web 接口均为资源
- 编程式:SphU API
- 声明式:@SentinelResource 注解
- 定义规则:
- 流量控制
- 熔断降级
- 系统保护
- 来源访问控制
- 热点参数限流
2、Sentinel 启动
在服务运行的机器中,通过命令行启动 sentinel,的 jar 包。
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # 指定 sentinel 控制台的 ip 和端口
注意:
- 放行端口:
- sentinel 启动时,首先会占用一个端口,后面会为每个引入的模块分配一个 端口, 端口号默认从 8719 开始,后面一次递增,因此服务器一定要开启
- 8719 等后面的端口。
- 公网 ip:
- 由于阿里云服务器只能连接访问到公网 ip,因此,如果在服务其上 ping 自己的内网 ip,是 ping 不通的。
3、Sentinel 流控资源分类
- 默认情况下,Sentinel 会自动为所有 web 接口生成资源,例如:/order/create。
- 如果需要手动指定资源,则需要使用 @SentinelResource 注解。
1、Web 接口
原理:通过拦截器进行拦截
// 继承 BlockExceptionHandler 接口,其实就是利用拦截器进行判断拦截
@Component
public class MyExceptionHandler implements BlockExceptionHandler {
// 用于将对象转化为jason字符串,springMVC自带的
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void handle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
String resourceName, BlockException e) throws Exception {
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter printWriter = httpServletResponse.getWriter();
// 自定义 web 限流返回信息
R ok = R.ok(500, resourceName + "资源的流量超出限制", e);
String json = objectMapper.writeValueAsString(ok);
printWriter.print(json);
}
}
2、@SentinelResource 注解
原理:SentinelResourceAspect 切面来进行,只要是添加了 @SentinelResource 注解的接口,就会有这个切面
切面内部的异常处理逻辑是:
- 先判断:blockHandler 是否有值
- 如果没有值,则在判断 fallback 是否有值
- 如果没有值,则判断 defaultFallback 是否有值
- 如果都没有值,则直接 throw 抛出给 SpringBoot 处理,可以通过全局异常处理捕获
@Override
@SentinelResource(value = "createOrder",blockHandler = "createOrderFallback")
public Order createOrder(Long productId, Long userId) {
// 正常逻辑
return order;
}
// 限流异常处理的兜底数据 BlockException 不是 Exception
public Order createOrderFallback(Long productId, Long userId, BlockException e) {
Order order = new Order();
order.setId(0L);
order.setTotalAmount(new BigDecimal("0.0"));
order.setUserId(userId);
order.setNickName("未知用户");
order.setAddress("错误:" + e.getClass());
return order;
}
3、OpenFeign 远程调用
如果出现限流控制,则会返回 OpenFeign 的远程调用在定义时的兜底回调,没有指定,则使用 SpringBoot 的异常处理
4、SphU 硬编码
3、常用的控制规则
1、流控规则
针对来源:default,表示所有来源,可以指定 ip 或者 serviceId
阀值类型:QPS:每秒调用次数
是否集群:可以配置每个服务器的流量控制
流控模式:
-
直接策略:直接限流,例如:当 QPS 超过阈值时,直接限流
-
链路策略:额可以指定入口资源,如果是普通的 creat 来的,不做限流,如果是 seckill 过来的都限流
-
关联策略:当该资源的关联资源请求量大时,限制该资源 \
注意:只有流控效果为快速失败
流控效果:
- 快速失败:直接限流,多余的丢弃
- Warm Up(预热):指定限流的最大值,当有大量请求时,会默认先让一部分(1/3)通过,然后慢慢增加,直到达到最大值
- 匀速排队:会按照QPS的指定数在 1s 上的等分,比如:QPS = 2 ,每秒 2 个请求,每500毫秒一个请求,因此指定的 QPS < 1000
同时,对于其他的请求会进项排队等待,但是有超时时间,默认为 20s,20s 后,也会丢弃
2、熔断规则
- 熔断降级:
如果当前微服务发现自己调用微服务时,总是抛出异常,那末,下一次在调用时,可以直接返回错误信息。
熔断降级作为自身的保护手段,通常在客户端(调用端)进行配置。
有利于:- 切断不稳定调用
- 快速返回步积压
- 避免服务雪崩,即由于链路中的某个微服务故障,同时有大量请求到来,导致服务器的资源耗尽,从而导致整个系统崩溃。
- 熔断开关工作原理:
- 首先,如果远程调用失败,那末直接返回错误信息,直到达到规定的阈值
- 断开状态:达到规定的阈值后,熔断开关会处于 断开状态 此时会拒绝所有请求,但是会有规定的断开时间
- 半闭合状态:如果达到规定的时间后,会进入 半闭合装填 此时,会先尝试放行一个请求,如果请求成功,并且是在合法的范围内(不被认为是慢调用等状态)
则进入打开状态,如果失败,则进入 断开状态再次重复循环。
- 熔断开关的阈值规定:
- 慢调用比例:设置慢时间,在统计时长内,所有请求中,请求时间大于慢时间的比例 大于 慢调用比例,断开
- 异常比例数:在统计时长内,在统计时长内,所有请求中,异常请求的比例 大于 异常比例数,断开
- 异常数:在统计时长内,请求的返回的是异常的请求个数 大于 异常数,断开
- 热点参数限流
注意:有熔断和没有熔断的区别
1、没有熔断:在进行远程调用的时候,发现超时或者远程调用的结果错误,那末执行兜底回调(每次都要向远程发请求)
2、有熔断:在进行远程调用的时候,出错了,那末执行兜底回调,并在熔断期间内,不会在向远程发送请求,而是直接返回兜底数据
八、gateway 网关
1、引入网关依赖
两个都需要引入,因为通过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-gateway</artifactId>
</dependency>
2、predicates 配置
routes 中的 predicates 属性是一个数组,即配置多种匹配规则,如下图所示。同时,对于每种匹配的参数信息,都保存在
RoutePredicateFactory 的实现类中,每个实现类删去后缀,就是规则的名字,而参数都在实现类的 Config 内部类中
如果添加多个匹配规则,那末只有所有的匹配规则都满足了,才会转发请求
例如:spring: cloud: gateway: routes: - id: order-route uri: lb://service-order # lb 表示使用负载均衡,会在nacos中查找对应的服务 predicates: - name: Path args: patterns: /api/order/** matchTrailingSlash: true # 是否精确比配,true:/xxx/1 和 /xxx/1/ 都匹配,false: /xxx/1 和 /xxx/1/ 不匹配 - name: Header # 向请求头中添加参数 args: header: token # 参数的名字 regexp: xxxxx # 参数的值,可以是正则表达式
同样,也可以自定义匹配规则,只需要写的跟 RoutePredicateFactory 的实现类类似即可
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://service-order # lb 表示使用负载均衡,会在nacos中查找对应的服务
predicates:
- Path=/api/order/**
- id: product-route
uri: lb://service-product
predicates:
- Path=/api/product/**
# - id: url-test
# uri: https://www.baidu.com # uri 可以是网址,既可以转发给其他的网页,直接访问该网址
# predicates:
# - Path=/**
- id: route-test
uri: http://localhost:8081
predicates:
- name: Path
注意:使用的是外部配置文件,需要在默认的配置文件中,包含外部配置文件,外部配置文件的命名规则为
application-{配置文件名}.yml
public List shortcutFieldOrder() {return Arrays.asList(“patterns”, “matchTrailingSlash”);}
表示短格式的参数,参数的顺序必须和这个方法返回的顺序一致,eg:- Path=/api/order/**,true
3、filters 配置
修改转发路径,添加请求头,修改请求参数,修改响应头等
spring: cloud: gateway: routes: - id: order-route uri: lb://service-order predicates: - Path=/api/order/**,true filters: # 路径重写,匹配 /api/order/** 的请求,将 /api/order/** 删除前缀 /api/order/ - RewritePath=/api/order/(?<path>.*),/$\{path} - id: product-route uri: lb://service-product predicates: - Path=/api/product/**,true filters: - RewritePath=/api/product/(?<path>.*),/$\{path} default-filters: # 默认过滤器,为所有的路由添加 - AddResponseHeader=default-filter, 默认值
自定义全局过滤器
@Slf4j @Component // 加入容器后生效 public class RtGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); String uri = request.getURI().toString(); long start = System.currentTimeMillis(); log.info("请求【{}】开始,时间:{}",uri,start); //===================以上是前置逻辑=================== // 由于是异步的,所以需要使用doFinally,而不是直接写在下边 Mono<Void> filter = chain.filter(exchange).doFinally(r ->{ //==================以下是后置逻辑=================== long end = System.currentTimeMillis(); log.info("请求【{}】结束,耗时:{}ms",uri,end-start); }); return filter; } // 顺序,返回的数值越小,优先级越高 @Override public int getOrder() { return 0; } }
自定义yml配置的过滤器
还是参考官方的配置类编写,继承 GatewayFilterFactory
或中其中的抽象类
再次实现抽象类的方法,例如继承:AbstractNameValueGatewayFilterFactory
类,这就是传入的是 参数名和参数值 \
开启全局跨域
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': # 对于所有请求以及请求类型都允许跨域操作 allowed-origin-patterns: '*' allowed-headers: '*' allowed-methods: '*'
九、Seata 分布式事务
TC (Transaction Coordinator) 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚
TM (Transaction Manager) 资源管理器:定义全局事务的范围,驱动全局事务提交或回滚
RM (Resource Manager):管理分支事务对应的资源,与 TC 交谈以注册分支事务和提交或回滚分支事务 \
1、Seata 的引入
客户端下载网址:https://seata.apache.org/zh-cn/download/seata-server
依赖的引入 \
<!--分布式事务-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
配置文件,所有引入 seata 的服务都需要配置这个文件,指定 seata 的服务器地址 \
service {
vgroupMapping.default_tx_group = "default"
default.grouplist = "localhost:8091"
enableDegrade = false
disableGlobalTransaction = false
}
2、Seata 的简单使用
在总的事务调用方法上添加 @GlobalTransactional
注解,其他分支事务添加 @Transactional
注解
3、Seata 的原理
通过二个阶段的提交,保证事务的一致性。
首先:每个微服务的事务开始执行,根据更新数据的结果,记录更前的数据和更新后的数据。
其次:单个微服务的事务提交前,先获取全局锁,保证没有其他的事务对数据进行操作。
然后:单个微服务的事务提交,这里已经更新数据库,并向数据库中的 undolog 日志插入了一条数据,用于记录提交前的数据和更新后的数据,并向全局事务报告事务的状态
最后:
- 如果所有事务的装填全部是成功,那末直接结束全局事务。
- 如果有一个事务的状态是回滚,那末,对于已经成功的事务,将根据undolog 日志,进行回滚(即再次修改为原来的数据),然后结束全局事务。
4、Seata 模式
- AT:默认的自动模式
- XA:自动模式,但是是根据数据库的XA协议进行实现的,利用数据库的事务做到全局事务,即全局事务在完成前,微服务的事务不会真的提交,而是阻塞
- TCC:手动模式,需要自己实现开启,提交,回滚,而 seata 只是在合适的时候调用这些方法,用于不仅仅是数据库的事务,也可以是广义上的事务
- SAGA:用于处理长事务