springCloud 单体应用 单体应用的特点
单体应用就是传统意义的,单个应用程序的应用
单体软件一般采用,分包的方式,来实现代码的解耦和管理
单体应用一般分为MVC三层架构,也可以分成表现层,业务层,持久层。本身起到一定的代码分割管理、方便维护
单体应用的特点是整个应用其实是一个web项目,是一个工程,运行在JVM(java虚拟机)
在单体应用中,springMvc(或者servlet)充当控制层 ,mabatis(或者JDBC) 充当持久层,Spring则充当整合表现层、业务层、持久层的作用
单体应用的缺陷
当项目越来越大,代码量越来越多,造成编译、打包费时,越来越影响效率。
当业务越来越多,不同的业务会重建新的项目,不同的项目的功能模块可能会出现重复建设的情况,造成浪费。
可伸缩性差. 单体应用中的功能模块的使用场景,并发量,消耗的资源类型各有不同(例如一个电商项目中,商品模块消耗的支援大,其他模块相对较少),对于资源的利用又互相影响,单体应用无法做到按照模块需求分配资源
系统错误隔离性差,可用性差.任何一个模块的错误均可能造成整个系统的宕机.
架构演变 SOA
定义
分布式架构
对一个复杂的业务系统进行垂直分层,每个垂直应用实际上是一个独立的子系统,它们共同组成了整个应用系统,这些子系统可以部署在不同的服务器上,这些服务器可以在不同的地域,当分布式应用之间的调用越来越多,整个系统的复杂度急剧上升,SOA可以降低分布式应用之间的耦合。
ESB服务总线 原理图
面向服务的架构思想
SAOP,REST,RPC 就是SOA指定的规范(落地方案)解决项目之前如何传送数据
基于http+xml(内容) -> SOAP(WEB service)->缺点:数据冗余
基于http+json->REST springCloud ->缺点 :安全性不高
基于Socker+文本 /二进制数据 -> dubbo 基于RPC->用字节流传输
Web server
定义:
Web Service是一个平台独立的,低耦合的,自包含的、基于可编程的web的应用程序,可使用开放的XML(标准通用标记语言下的一个子集)标准来描述、发布、发现、协调和配置这些应用程序,用于开发分布式的交互操作的应用程序
举例:调用天气信息
WeatherWS Web 服务 (webxml.com.cn)
返回的数据:
<ArrayOfString xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd ="http://www.w3.org/2001/XMLSchema" xmlns ="http://WebXml.com.cn/" > <string > 内蒙古 呼和浩特</string > <string > 呼和浩特</string > <string > 355</string > <string > 2023/11/17 19:31:52</string > <string > 今日天气实况:气温:-2.5℃;风向/风力:西北风 2级;湿度:43%</string > <string > 紫外线强度:强。</string > <string > 感冒指数:易发,天冷易感冒,注意防范。 运动指数:较不宜,天气寒冷,推荐您进行室内运动。 过敏指数:极不易发,无需担心过敏,可放心外出,享受生活。 穿衣指数:冷,建议着棉衣加羊毛衫等冬季服装。 洗车指数:适宜,天气较好,适合擦洗汽车。 紫外线指数:强,涂擦SPF大于15、PA+防晒护肤品。 </string > <string > 11月17日 晴</string > <string > -8℃/5℃</string > <string > 西风转西南风小于3级</string > <string > 0.gif</string > <string > 0.gif</string > </ArrayOfString >
技术栈
XML(标准通用标记语言下的一个子集):XML是在web上传送结构化数据的伟大方式,Web services要以一种可靠的自动的方式操作数据,HTML(标准通用标记语言下的一个应用)不会满足要求,而XML可以使web services十分方便的处理数据,它的内容与表示的分离十分理想
SOAP:SOAP使用XML消息调用远程方法,这样web services可以通过HTTP协议的post和get方法与远程机器交互,而且,SOAP更加健壮和灵活易用
Web Service描述语言WSDL 就是用机器能阅读的方式提供的一个正式描述文档而基于XML(标准通用标记语言下的一个子集)的语言,用于描述Web Service及其函数、参数和返回值。因为是基于XML的,所以WSDL既是机器可阅读的,又是人可阅读的。
微服务 微服务是SOA的一种落地方案,SOA是一种面向服务的架构思想,微服务也同样推崇这种思想.
微服务架构
前提:要有粗略的框架(脚手架)->spring boot 为什么微服务要结合云技术 什么是云 云服务是一种通过互联网提供各种资源和服务的模式,例如计算、存储、数据库、分析、机器学习等。云服务可以根据用户的需求动态地扩展或缩减,而无需管理底层的硬件或软件
云服务的种类
基础设施即服务(IaaS):提供简化的基础设施管理,大规模的水平可伸缩性,通过地理分布实现高冗余,可以跨多个云供应商进行移植并且允许开发人员通过产品覆盖更广泛的受众。
平台即服务 (PaaS)
软件即服务 ( SaaS)
区别 各种服务暴露的情况
云和微服务
核心概念:就是每个服务都被打包和部署为离散的独立程序。服务实例应迅速启动,服务的每一个实例都是完全相同的。所以服务都将部署到以下某个环境之中.
优点:能够快速启动和关闭微服务实例,以响应可伸缩性和服务故障事件. 简单来说就是快速部署
基于云的微服务以弹性的概念为中心。在需要时,可以在云上几分钟之内快速布置启动新的虚拟机和容器,如服务需求下降,可以关闭虚拟服务器。这样可显著提高应用程序的水平可伸缩性,也使应用程序更有弹性.
微服务开发要点
位置透明
微服务之间的通信不依赖具体的物理位置,而是通过服务注册和发现机制,动态的获取服务的地址和端口 解决方案 eurka、nacos
有弹性/可伸缩
指微服务应该具备在面对各种故障和压力时,能够保持正常运行或快速恢复的能力。
服务的可观察性:能够通过日志、监控、跟踪等手段,实时了解服务的运行状态、性能指标、异常情况等,以便及时发现和定位问题。
服务的容错性:即能够通过熔断、降级、重试等策略,避免单个服务的故障导致整个系统的不可用或性能下降(服务器负载不起,拒绝一些请求,或者告诉请求方多长时间后处理好)
服务的伸缩性:能够通过负载均衡、自动扩缩容等机制,根据请求量的变化,动态调整服务的数量和分配,以满足不同的负载需求。
服务的可恢复性:即能够通过备份、回滚、灾备等措施,在发生严重故障时,能够快速恢复服务的正常运行。
可重复
将这些公共的部分抽取出来,封装成公共模块或服务(产品,API),供其他模块或服务调用。这样可以提高代码的复用性、可维护性和一致性,减少冗余和错误。
微服务模式
特点
微服务是一种架构风格,将一个复杂的应用拆分成多个独立自治的服务,服务与服务间通过松耦合的形式交互,通常采用轻量级的协议
微服务遵循单一职责原则,每个服务只负责一个业务领域或功能,服务的粒度较小,便于开发、测试和部署。
微服务支持技术异构性,不同的服务可以采用不同的编程语言、框架和数据存储技术,根据业务需求和团队能力进行选择。
微服务具有高可扩展性,可以根据不同服务的负载情况进行水平扩展或缩减,提高资源利用率和性能。
微服务具有高可靠性,一个服务的故障不会影响整个系统的运行,可以通过隔离、熔断等机制提高系统的容错能力。
微服务具有灵活组合性,可以通过组合已有的服务来实现新的功能或业务场景,提高复用性和开发效率。
优势
可以提高开发效率和部署速度
可以提高系统的可靠性和容错性
微服务架构可以提高系统的可扩展性和性能
可以提高系统的技术多样性和创新性
提高系统的灵活组合性和复用性
挑战
服务太多,依赖复杂,运维难度大
运维复杂度增加,部署服务数量多,监控进程多导致整体运维复杂度提升
SpringCloud简介
cloud与微服务的关系:微服务是思想, cloud是解决方案
cloud是一系列框架的集合
版本
cloud 2
cloud1
服务注册与发现
nacos
eureka
服务远程调用
openFeign
Feign
服务降级\服务熔断\服务限流
sentinel
hystrix
分布式事务
seata
\
配置中心
nacos
spring cloud config
总线
nacos
stream
网关
gateway
gateway
链路追踪
sleuth
sleuth
……
……
……
服务注册与发现 CAP
一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项 分布式系统一定满足分区容错性
一致性是指所有节点在同一时间看到的数据是相同的,即数据的正确性和一致性得到保证
可用性是指每个请求都能得到响应,不会出现错误或超时,即服务的可靠性和响应性得到保证
分区容错性是指当网络发生故障或延迟时,系统仍然能够继续运行,不会挂掉,即系统的稳定性和容错性得到保证
对于涉及到钱财等敏感数据的场景,需要保证一致性C和分区容错性P(牺牲可用性)
对于大型互联网应用的场景,需要保证可用性A和分区容错性P(牺牲一致性),并尽可能实现最终一致性
常见产品的区分
Nacos 注册服务发现 作用: 每个系统引入注册中心后,可以把自己的信息注册到nacos ,我们假设订单系统要去调用库存系统,订单系统可以拿到库存系统对应服务的名称,而库存系统在注册中心注册了自己的信息,订单系统可以通过名称去拿到库存系统服务的端口号和ip地址,再通过负载均衡或长轮询去选择用那个
官方文档:https://nacos.io/zh-cn/docs/architecture.html
启动nacos:命令窗口进入nacos的bin目录下 执行
startup.cmd -m standalone -- 默认为集群方式打开 standalone为单体打开
可通过localhost:8848/nacos 进入他的页面
nacos服务的工作流程
配置管理:应用或服务可以向Nacos服务器发布或获取配置信息,如应用参数、路由规则、灰度策略等。Nacos服务器会保存配置信息到内存或数据库中,并根据长轮询的方式推送配置变更通知。长轮询:长轮询是一种服务器选择尽可能长的时间保持和客户端连接打开的技术,仅在数据变得可用或达到超时阙值后才提供响应
如何通过http 发布或者获取配置
发布: curl -X POST “http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test&content=HelloWorld “
获取:curl -X GET “http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test “
DNS服务:应用或服务可以向Nacos服务器查询域名对应的IP列表,实现基于DNS协议的服务发现和负载均衡。Nacos服务器会根据权重路由等策略返回IP列表,并缓存DNS记录。
元数据管理:应用或服务可以向Nacos服务器查询或更新元数据信息,如服务描述、生命周期、依赖关系、健康状态、流量管理等。Nacos服务器会提供可视化的仪表盘和API来管理元数据信息。
服务注册发现:
服务注册:Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如IP地址、端口等信息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内测Map中。
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.10&port=8080'
服务心跳-检测:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。
服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。Nacos Server 集群是为了防止单点故障
服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请NacosServer,获取上面注册的服务清单,并且缓存在NacosClient本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存 Spring
服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)
spring cloud alibaba版本必须依赖spring cloud版本,可在官方文档中查找
Spring Boot
Spring Cloud
start.spring.io/actuator/info
版本说明 · alibaba/spring-cloud-alibaba Wiki (github.com)
使用注册中心
<dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency >
注意:这里因为依赖仲裁,所以不需要指定版本
因为他是一个starter,肯定有配置项,可以参考官方文档去设置
server: port: 9004 spring: cloud: nacos: discovery: server-addr: localhost:8848 application: name: resorder
一个类启多个服务怎么搞?增加Boot配置后 给他加一个虚拟机配置
-Dserver .port=9001 -- 修改端口即可启动多个服务
我们通过nacos暴露的端口在注册中心查看
构建一个消费模块 同样需要注册和加入注解
将RestTemplate交给Spring托管
package com.y.config;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 webconfig { @LoadBalanced @Bean public RestTemplate restTemplate () { return new RestTemplate (); } }
将RestTemplate注入到对应的控制类 调用其中的方法去发请求
package com.y.wen.controller;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;import javax.servlet.http.HttpSession;import java.util.HashMap;import java.util.Map;@RequestMapping("order") @RestController @Slf4j public class ordercontoller { @Autowired public RestTemplate restTemplate; @RequestMapping(value = "addCart",method = {RequestMethod.POST}) public Map<String, Object> addCart (@RequestParam Integer fid, @RequestParam Integer num, HttpSession httpSession) { Map<String,Object> map=new HashMap <>(); Map <String,Object> result = this .restTemplate.getForObject("http://localhost:9001/food/findByFid?fid=" + fid,Map.class ); log.info("发送请求后得到商品信息" +result); return map; } }
logging: level: root: info org.springframework.web.client: debug org.apache: error file: path: logs
loadbalance(负载均衡)
负载均衡是对系统的高可用、网络峰值压力的缓解和处理能力扩容的重要手段之一,是分布式系统基础架构
loadbalancer使用Reactor模式实现响应式编程,提高了性能和可扩展性
loadbalancer提供的负载均衡有2种,默认的轮询(RoundRobinLoadBalancer)和随机(RandomLoadBalancer)
实现原理
维护一个下挂可用的服务端清单,通过心跳检测来剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。当客户端发送请求到负载均衡设备的时候,该设备按某种算法(比如线性轮询、按权重负载、按流量负载等)从维护的可用服务端清单中取出一台服务端端地址,然后进行转发.
负载均衡两种实现方法
服务器端负载均衡:nginx 自身保存服务清单, 客户端的所有请求统一交给 nginx,由 nginx 进行实现负载均衡请求转发
客户端负载均衡:LoadBalancer 是从 Nacos 注册中心服务器端上获取服务注册信息列表,缓存到本地,然后在本地实现负载均衡策略
使用
对全局( 所有服务 )RestTemplate注解@LoadBalanced
@Configuration public class webconfig { @LoadBalanced @Bean public RestTemplate restTemplate () { return new RestTemplate (); } }
导入loadbalance的依赖,父容器托管不需要版本
<dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-loadbalancer</artifactId > </dependency >
@RequestMapping("order") @RestController @Slf4j public class ordercontoller { @Autowired public RestTemplate restTemplate; @RequestMapping(value = "addCart",method = {RequestMethod.POST}) public Map<String, Object> addCart (@RequestParam Integer fid, @RequestParam Integer num, HttpSession httpSession) { String url="http://resfood/food/findByFid?fid=" ; Map<String,Object> map=new HashMap <>(); Map <String,Object> result = this .restTemplate.getForObject(url + fid,Map.class ); log.info("发送请求后得到商品信息" +result); return map; } }
loadbalance是如挂载的 loladbalance源码分析
创建负载均衡 原理已经了解了,下面仿照源码实现一个简单的负载均衡
实现ReactorServiceInstanceLoadBalancer 接口,实现负载均衡算法,下面基本是复制官方的代码,讲一下如何实现
package com.y.config;import org.springframework.beans.factory.ObjectProvider;import org.springframework.cloud.client.ServiceInstance;import org.springframework.cloud.client.loadbalancer.DefaultResponse;import org.springframework.cloud.client.loadbalancer.EmptyResponse;import org.springframework.cloud.client.loadbalancer.Request;import org.springframework.cloud.client.loadbalancer.Response;import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;import org.springframework.cloud.loadbalancer.core.SelectedInstanceCallback;import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;import reactor.core.publisher.Mono;import java.util.List;import java.util.Random;public class OnlyOneLoadBalancer implements ReactorServiceInstanceLoadBalancer { String serviceId; ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider; public OnlyOneLoadBalancer (ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) { this .serviceInstanceListSupplierProvider=serviceInstanceListSupplierProvider; } public OnlyOneLoadBalancer (ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) { this .serviceId=serviceId; this .serviceInstanceListSupplierProvider=serviceInstanceListSupplierProvider; } @Override public Mono<Response<ServiceInstance>> choose (Request request) { ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this .serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new ); return supplier.get(request).next().map((serviceInstances) -> { return this .processInstanceResponse(supplier, serviceInstances); }); } private Response<ServiceInstance> processInstanceResponse (ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) { Response<ServiceInstance> serviceInstanceResponse = this .getInstanceResponse(serviceInstances); if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) { ((SelectedInstanceCallback)supplier).selectedServiceInstance((ServiceInstance)serviceInstanceResponse.getServer()); } return serviceInstanceResponse; } private Response<ServiceInstance> getInstanceResponse (List<ServiceInstance> instances) { if (instances.isEmpty()) { return new EmptyResponse (); } else { ServiceInstance instance = (ServiceInstance)instances.get(0 ); return new DefaultResponse (instance); } } }
托管这个类,仿照LoadBalancerClientFactory这个类
package com.y.config;import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration;import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.env.Environment;@Configuration public class MyLoadBalanceConfig { @Bean public ReactorServiceInstanceLoadBalancer myOnlyOneReactorServiceInstanceLoadBalancer (Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); return new OnlyOneLoadBalancer (loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class)); } }
要用就要去配置 要在使用了@loadbalancer注解的类上配置两种方案
@LoadBalancerClient(name = "resfood", configuration = MyLoadBalanceConfig.class)
@LoadBalancerClients( value = { @LoadBalancerClient(value = "resfood", configuration = MyLoadBalanceConfig.class) }, defaultConfiguration = LoadBalancerClientConfiguration.class )
openfeign
RestTemplate的不足:我们之前发请求时是将请求路径写死,这种方式 编译成字节码文件后无法修改,且编写相对繁琐,我们应该用面向对象的方式去实现 ,openfeign帮我们解决这个问题
之前用RestTemplate 底层就是发一个请求,url还需要我们自己编写
而openfeign 只需要定义服务接口,他会生成对应的代理对象
openfeign 会将服务的相应转换成Java对象并返回
Spring Cloud OpenFeign是一个声明式的REST客户端,它可以让我们用注解的方式来调用其他服务的接口。它基于Feign,并支持Spring MVC的注解和Spring Web的HttpMessageConverters。
动态代理补充 在测试类中使用会生成代理对象的字节码文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles" ,"true" );
package com.y;public class Test { public static void main (String[] args) { System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles" ,"true" ); Hello target=new HelloImpl (); CustomInvocationHandler handler=new CustomInvocationHandler (target); Object proxy=handler.createProxy(); System.out.println(proxy); Hello hi=(Hello) proxy ; hi.sayHello(); hi.sayBye(); } }
会在根目录下生成代理对象的字节码文件和他的目录com/sun/porxy
package com.sun.proxy;import com.y.Hello;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.lang.reflect.UndeclaredThrowableException;public final class $Proxy0 extends Proxy implements Hello { private static Method m1; private static Method m4; private static Method m2; private static Method m3; private static Method m0; public $Proxy0(InvocationHandler var1) throws { super (var1); } public final boolean equals (Object var1) throws { try { return (Boolean)super .h.invoke(this , m1, new Object []{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException (var4); } } public final void sayHello () throws { try { super .h.invoke(this , m4, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException (var3); } } public final String toString () throws { try { return (String)super .h.invoke(this , m2, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException (var3); } } public final void sayBye () throws { try { super .h.invoke(this , m3, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException (var3); } } public final int hashCode () throws { try { return (Integer)super .h.invoke(this , m0, (Object[])null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException (var3); } } static { try { m1 = Class.forName("java.lang.Object" ).getMethod("equals" , Class.forName("java.lang.Object" )); m4 = Class.forName("com.y.Hello" ).getMethod("sayHello" ); m2 = Class.forName("java.lang.Object" ).getMethod("toString" ); m3 = Class.forName("com.y.Hello" ).getMethod("sayBye" ); m0 = Class.forName("java.lang.Object" ).getMethod("hashCode" ); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError (var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError (var3.getMessage()); } } }
什么是Feign
是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。
通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。
解决了什么问题
封装 HTTP 调用流程,面向接口编程但做了大量的适配工作
feign 声明式注解
Annotation
Interface Target
Usage
@RequestLine
Method
定义HttpMethod 和 UriTemplate. UriTemplate 中使用{} 包裹的表达式,可以通过在方法参数上使用@Param 自动注入
@Param
Parameter
定义模板变量,模板变量的值可以使用名称的方式使用模板注入解析
@Headers
Method, Type
定义头部模板变量,使用@Param 注解提供参数值的注入。如果该注解添加在接口类上,则所有的请求都会携带对应的Header信息;如果在方法上,则只会添加到对应的方法请求上
@QueryMap
Parameter
定义一个Map或 POJO,参数值将会被转换成URL上的 query 字符串上
@HeaderMap
Parameter
Map ->Http Headers
@Body
Method
定义一个模板,类似于UriTemplate和HeaderTemplate,它使用@Param注释值来解析相应的表达式。
public interface FeignService { @RequestLine("GET /api/documents/{contentType}") @Headers("Accept: {contentType}") String getDocumentByType (@Param("contentType") String type) ; @RequestLine("GET /find") V find (@QueryMap Map<String, Object> queryMap) ; @RequestLine("GET /find") V find (@QueryMap CustomPojo customPojo) ; @RequestLine("POST /") void post (@HeaderMap Map<String, Object> headerMap) ; @RequestLine("POST /") @Headers("Content-Type: application/xml") @Body("<login \"user_name\"=\"{user_name}\" \"password\"=\"{password}\"/>") void xml (@Param("user_name") String user, @Param("password") String password) ; @RequestLine("POST /") @Headers("Content-Type: application/json") @Body("%7B\"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") void json (@Param("user_name") String user, @Param("password") String password) ; }
使用
<dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency >
配置要暴露的api接口 在对应的l上加注解@FeignClient(”这里是注册的项目名”) 然后给对应的接口配置请求路径
package com.y.api;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RequestParam;import java.util.Map;@FeignClient("resfood") public interface ResFoodApi { @RequestMapping("food/detailCountAdd") public Map<String,Object> detailCountAdd (Integer fid) ; @RequestMapping("food/findAll") public Map<String,Object> findAll () ; @RequestMapping("food/findByFid") public Map<String,Object> findById (@RequestParam Integer fid) ; @RequestMapping(value = "food/findByPage",method =RequestMethod.POST) public Map<String,Object> findByPage (@RequestParam int pageno,@RequestParam int pagesize, @RequestParam String sortby,@RequestParam String sort) ;}
要在对应的消费模块的启动类上加EnableFeignClients,这里是订单
package com.y;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 @EnableDiscoveryClient @EnableFeignClients(basePackages= {"com.y.api"}) public class ResOrderApp { public static void main (String[] args) { SpringApplication.run(ResOrderApp.class,args); } }
package com.y.web.controller;import com.y.api.ResFoodApi;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;import javax.servlet.http.HttpSession;import java.util.HashMap;import java.util.Map;@RequestMapping("order") @RestController @Slf4j public class ordercontoller { @Autowired public ResFoodApi resFoodApi; @Autowired public RestTemplate restTemplate; @RequestMapping(value = "addCart",method = {RequestMethod.POST}) public Map<String, Object> addCart (@RequestParam Integer fid, @RequestParam Integer num, HttpSession httpSession) { Map<String,Object> map=new HashMap <>(); Map<String,Object> result=this .resFoodApi.findById(fid); log.info("发送请求后得到商品信息" +result); return map; } }
配置中心(nacos)
没统一存放配置之前,有个明显的问题,就是当修改了配置之后。必须重启服务,否则配置会失效。
启动配置文件,不应该加到配置中心
处理的问题
集中管理配置文件
不同坏境不同配置,动态化的配置更新
运行期间,不需要去服务器修改配置文件,服务会从配置中心拉取自己的信息
配置信息以 rest 接口暴露
架构图
配置文件到测试环境
在nacos创建命名空间,在此命名空间下创建配置
<dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-bootstrap</artifactId > </dependency >
这个bootstrap的依赖 是因为你要告诉它你的配置信息在哪里,否则会读不到你在配置中心的配置
在resource下新建bootstrap.yml文件在里面加入下面依(bootstrap.yml用来程序引导时执行,比application.yml先加载,应用于更加早期配置信息读取,可以理解成系统级别的一些参数配置,这些参数一般是不会变动的。一旦bootStrap.yml被加载,则内容不会被覆盖)
spring: cloud: nacos: config: server-addr: localhost:8848 namespace: res134 group: DEFAULT_GROUP username: nacos password: nacos prefix: resfood file-extension: yml profiles: active: dev
2023 -11-29 19 :43 :09.955 INFO 16680 --- [ restartedMain ] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729 2023 -11-29 19 :43 :10.215 INFO 16680 --- [ restartedMain ] o.s.b.a.e.web.EndpointLinksResolver : Exposing 20 endpoint(s) beneath base path '/actuator' 2023 -11-29 19 :43 :10.276 INFO 16680 --- [ restartedMain ] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 9000 (http) with context path '' 2023 -11-29 19 :43 :10.285 INFO 16680 --- [ restartedMain ] c.a.n.p.a.s.c.ClientAuthPluginManager : [ClientAuthPluginManager ] Load ClientAuthService com.alibaba.nacos.client.auth.impl.NacosClientAuthServiceImpl success.2023 -11-29 19 :43 :10.286 INFO 16680 --- [ restartedMain ] c.a.n.p.a.s.c.ClientAuthPluginManager : [ClientAuthPluginManager ] Load ClientAuthService com.alibaba.nacos.client.auth.ram.RamClientAuthServiceImpl success.2023 -11-29 19 :43 :10.415 INFO 16680 --- [ restartedMain ] c.a.c.n.registry.NacosServiceRegistry : nacos registry, DEFAULT_GROUP resfood 127.0 .0.1 :9000 register finished2023 -11-29 19 :43 :10.721 INFO 16680 --- [ restartedMain ] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing2023 -11-29 19 :43 :10.727 INFO 16680 --- [ restartedMain ] com.y.ResFoodApp : Started ResFoodApp in 6.633 seconds (JVM running for 7.355 )2023 -11-29 19 :43 :10.737 INFO 16680 --- [ restartedMain ] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config ] Listening config: dataId=resfood, group =DEFAULT_GROUP2023 -11-29 19 :43 :10.738 INFO 16680 --- [ restartedMain ] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config ] Listening config: dataId=resfood.yml, group =DEFAULT_GROUP2023 -11-29 19 :43 :10.738 INFO 16680 --- [ restartedMain ] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config ] Listening config: dataId=resfood-dev .yml, group =DEFAULT_GROUP2023 -11-29 19 :43 :11.029 INFO 16680 --- [on (1 )-127.0 .0.1 ] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2023 -11-29 19 :43 :11.031 INFO 16680 --- [on (1 )-127.0 .0.1 ] o.s.web.servlet.DispatcherServlet : Completed initialization in 2 ms
动态刷新
2023-11-30 20:54:02.341 INFO 28248 --- [ternal.notifier] o.s.c.e.event.RefreshEventListener : Refresh keys changed: [res.pattern.dataFormat]
注意:使用动态刷新,确保你引入了actuator依赖并开放了resfreshScope端点
实际上就是nacos服务器向actuator/refresh发了个请求
共享配置
多个模块需要用到相同的配置,列如连接数据库,这时可以吧公共的配置提取出来,放到一个新建的配置里,再在bootstarp.yml中加载这个配置
这里将mysql和redis提取出来,记得要把之前配置文件关于MySQL和redis的内容删掉
注意这里命名规范,前面的名字顺便取,但是一定要以.yml结尾
在bootstarp.yml下新增共享文件配置shared-configs
spring: cloud: nacos: config: server-addr: localhost:8848 namespace: res134 group: DEFAULT_GROUP username: nacos password: nacos prefix: resfood file-extension: yml shared-configs: - {data-id: 'mysqllocal.yml' ,refresh: true } - {data-id: 'redislocal.yml' ,refresh: true } profiles: active: prod
refresh 表示是否启用动态刷新 ,group可以不用配,他会给你默认的
启动时控制台会打印如下信息
2023 -11-30 21 :32 :03.224 INFO 17716 --- [ restartedMain ] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729 2023 -11-30 21 :32 :03.484 INFO 17716 --- [ restartedMain ] o.s.b.a.e.web.EndpointLinksResolver : Exposing 20 endpoint(s) beneath base path '/actuator' 2023 -11-30 21 :32 :03.541 INFO 17716 --- [ restartedMain ] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8000 (http) with context path '' 2023 -11-30 21 :32 :03.550 INFO 17716 --- [ restartedMain ] c.a.n.p.a.s.c.ClientAuthPluginManager : [ClientAuthPluginManager ] Load ClientAuthService com.alibaba.nacos.client.auth.impl.NacosClientAuthServiceImpl success.2023 -11-30 21 :32 :03.551 INFO 17716 --- [ restartedMain ] c.a.n.p.a.s.c.ClientAuthPluginManager : [ClientAuthPluginManager ] Load ClientAuthService com.alibaba.nacos.client.auth.ram.RamClientAuthServiceImpl success.2023 -11-30 21 :32 :03.675 INFO 17716 --- [ restartedMain ] c.a.c.n.registry.NacosServiceRegistry : nacos registry, DEFAULT_GROUP resfood 173.18 .22.84 :8000 register finished2023 -11-30 21 :32 :03.988 INFO 17716 --- [ restartedMain ] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing2023 -11-30 21 :32 :03.993 INFO 17716 --- [ restartedMain ] com.y.ResFoodApp : Started ResFoodApp in 6.814 seconds (JVM running for 7.541 )2023 -11-30 21 :32 :04.003 INFO 17716 --- [ restartedMain ] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config ] Listening config: dataId=resfood, group =DEFAULT_GROUP2023 -11-30 21 :32 :04.004 INFO 17716 --- [ restartedMain ] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config ] Listening config: dataId=redislocal.yml, group =DEFAULT_GROUP2023 -11-30 21 :32 :04.004 INFO 17716 --- [ restartedMain ] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config ] Listening config: dataId=resfood.yml, group =DEFAULT_GROUP2023 -11-30 21 :32 :04.005 INFO 17716 --- [ restartedMain ] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config ] Listening config: dataId=mysqllocal.yml, group =DEFAULT_GROUP2023 -11-30 21 :32 :04.005 INFO 17716 --- [ restartedMain ] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config ] Listening config: dataId=resfood-prod .yml, group =DEFAULT_GROUP2023 -11-30 21 :32 :04.505 INFO 17716 --- [2 )-173.18 .22.84 ] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2023 -11-30 21 :32 :04.507 INFO 17716 --- [2 )-173.18 .22.84 ] o.s.web.servlet.DispatcherServlet : Completed initialization in 2 ms
数据库迁移
现在用的数据源是druid,在依赖中引入了druid的starter,他会帮我们自动配置,要想实现动态刷新,要在对应的类或者方法上加上@RefreshScope注解,但是这个数据源不是我们自己创建的,他已经被编译成了字节码文件,那么我们想实现动态刷新,就不能用starter的方案,要自己创建一个数据源
package com.y.config;import com.alibaba.druid.pool.DruidDataSource;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.cloud.context.config.annotation.RefreshScope;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import javax.sql.DataSource;@Configuration @Slf4j @RefreshScope public class MyDruidDataSource { @Value("${spring.datasource.url}") private String url; @Value("${spring.datasource.driver-class-name}") private String driverClassName; @Value("${spring.datasource.username}") private String username; @Value("${spring.datasource.password}") private String password; @Bean @Primary @RefreshScope public DataSource druid () { log.info("使用的编程式的数据源创建." ); DruidDataSource ds=new DruidDataSource (); ds.setUsername(username); ds.setPassword(password); ds.setDriverClassName(this .driverClassName); ds.setUrl(url); return ds; } }
类上面加是实现属性动态刷新,属性变时,druid会重建
@Primary表示如果有多个数据源要优先使用这个
更改配置后他会动态刷新创建一个新的SQLSession
控制台打印信息如下
2023 -11-30 23 :10 :04.596 INFO 27032 --- [ternal.notifier ] o.s.c.e.event.RefreshEventListener : Refresh keys changed: [spring.datasource.url ]Creating a new SqlSession
服务哨兵(sentinel)
主要完成一些流量控制 、服务治理,解决服务雪崩问题
以流量为切入点,从流量控制熔断降级,系统负载保护等多维度保护服务的稳定性
组件图
特点:
多种限流算法:包括令牌桶、漏桶等,可以根据业务场景选择合适的算法。
多种限流维度:包括QPS、并发线程数、异常比例等,可以根据不同的维度来进行限流。
多种应用场景:支持Dubbo、Spring Cloud、gRPC等多种RPC框架的服务发现和调用。
动态规则源:支持多种数据源,如Nacos、Zookeeper、Apollo等,可以动态推送和更新规则。
实时监控:提供实时的监控和统计功能,可以查看服务的运行状态和指标、
程序结构 执行原理
将不同的Slot按照顺序串在一起(责任链模式),从而将不同的功能(熔断、降级等)组合在一起。
并发测试
评估系统在同时处理多个用户请求时的性能。在这种测试 中,系统会暴露于一定数量的用户负载下,并且会记录系统的响应时间、吞吐量和资源利用率等指标。
线程数:对应的并发用户
Ramp Up 时间 线程准备时长,对应测试时间。例如线程数为10,准备时长为10 就是一秒启动线程
循环次数:代表每个线程发多少次请求
jmeter的使用
使用前保证项目环境可用
一个线程组表示一组用户
测试对应的服务,新建http请求项
查看测试结果
可以在工具里使用函数助手 列如random 可以让参数不固定
聚合报告:提供有关事务响应时间、吞吐量和错误率的信息。
查看结果树:显示每个请求的响应,包括请求头、请求正文和响应正文。
监听器图形结果:将测试结果可视化,以便更轻松地分析性能问题。
断言结果:验证响应是否满足特定条件。
分布式负载测试图:显示不同服务器上的负载情况
服务雪崩 什么是雪崩
服务雪崩就是服务提供者不可用导致服务调用者不可用,并将不可用逐渐放大。换句话说如果服务调用是链式调用,假如一个服务失败,导致整条链路的服务都失败的情形,我们称之为服务雪崩。
发生服务雪崩的原因和阶段
第一阶段:硬件故障、程序bug、缓存击穿(系统会缓存你最近访问的数据,假如你访问的数据在缓存中没有,导致请求会去数据库中查询,这样会让性能下降,可能导致一些服务不可用)、用户大量请求
第二阶段 :调用段可能因为网络问题不断的刷新,加大流量和并发可能性(用户重试)
第三阶段: 服务调用者不可用:导致资源被耗尽
解决方案
出现缓存击穿:1. 缓冲预热:提前将一些热点数据存入缓存中 2.提前筛选:不让错误的请求定位到缓冲
用户大量请求:做筛选,可以限制同一个ip的多次访问
超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止的等待
服务降级:拒绝服务,直接返回错误信息、页面拒绝、延迟持久化(将数据缓冲起来,不立马去操作数据库修改数据)
服务熔断
当下游的服务因为某种原因突然变得不可用或响应过慢,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用
服务熔断是服务降级的一种
断路器模式
断路器模式可以比作一个空气开关,当压力过大时,自动断开,待压力变小时再恢复工作
原理
原理: 当远程服务被调用时,断路器将监视这个调用,如调用时间太长,断路器将会介入并中断调用。此外,断路器将监视所有对远程资源的调用,如对某一个远程资源的调用失败次数足够多,那么断路器会出现并采取快速失败,阻止将来调用失败的远程资源.
状态图
当一个请求访问时。断路器默认是关闭的,你的请求可以到对应的服务上去,如果流量突然增大或者底层服务不可调用时,请求次数达到失败阈值,断路器会从close状态变为open状态,此时请求无法到达服务上,当处于open状态时,会休眠五秒,休眠结束后会尝试关闭,将一小部分请求放过去,试一试服务是否可用,如果可用则变为关闭状态,不可用则返回为open状态
状态统计:滑动窗口:统计最近访问的请求数量/统计一定时间内请求数量
服务降级
当下游服务因为某种原因响应过慢,下游服务主动停掉一些不重要的服务,释放服务器资源,增加响应速度
当下游服务因为某些原因不可用时,上游主动调用一些本地的降级逻辑,避免卡顿,迅速返回信息给用户
非关键服务保证服务ap(可用) 减轻cp(一致性)
舱壁模式
规范每个业务用的线程数,避免耗尽资源,尽量减少崩坏服务对其他服务的影响
sentinel的使用
<dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-core</artifactId > <version > 1.8.6</version > </dependency > <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-transport-simple-http</artifactId > <version > 1.8.6</version > </dependency >
simple-http:是一个简版的http服务器,用于与dashboard通讯 默认端口9999
定义资源
public static void main (String[] args) { initFlowRules(); while (true ){ try (Entry entry= SphU.entry("HelloWorld" )){ System.out.println("要执行的操作。。。" ); } catch (BlockException e) { System.out.println("blocked!" ); } }
定义资源也可以使用注解**@SentinelResource**(“HelloWorld”)
定义规则
private static void initFlowRules () { List<FlowRule> rules = new ArrayList <>(); FlowRule rule = new FlowRule (); rule.setResource("HelloWorld" ); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(100000 ); rules.add(rule); FlowRuleManager.loadRules(rules); }
-Dcsp .sentinel.dashboard.server=localhost:9999
效果如下
sentinel客户端整合springcloud
<dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-sentinel</artifactId > </dependency >
spring: cloud: sentinel: transport: port: 8719 dashboard: localhost:9999 eager: true
@SpringBootApplication public class Application { public static void main (String[] args) { SpringApplication.run(ServiceApplication.class, args); } } @Service public class TestService { @SentinelResource(value = "sayHello") public String sayHello (String name) { return "Hello, " + name; } } @RestController public class TestController { @Autowired private TestService service; @GetMapping(value = "/hello/{name}") public String apiHello (@PathVariable String name) { return service.sayHello(name); } }
@SentinelResource
注解用来标识资源是否被限流、降级
控制层不需要 @SentinelResource
注解因为在 Web 层直接使用 Spring Cloud Alibaba 自带的 Web 埋点适配。而业务层需要加 @SentinelResource
注解加到服务实现上,列如上面的sayHello
现在可以直接在客户端中查看(这里是自己的项目) ,里面所有访问服务的请求都会在下面列出来
服务流控
服务流控模式 关联控制
当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量
访问优先请求时超过10个则限制被限请求
如果用排队则将被限请求放到等待队列中,在超时时间内,待优先请求执行完后再执行
测试结果
链路流控
资源通过调用关系,相互之间构成一棵调用树,如图所示
machine-root / \ / \ Entrance1 Entrance2 / \ / \ DefaultNode(nodeA) DefaultNode(nodeA)
上图用两个入口分别调用nodeA,通过链路可只统计入口1的请求,这样可以对入口1的请求进行限流
链路限流底层默认将每条链路都统计,我们在测试时要在appilcation.yml中sentinel下加入 web-content-unify: flase
关闭链路整合的功能
测试在订单控制层新加两个方法,让他们调用同一个服务,代码如下
@Autowired public GoodsBiz goodsBiz; @RequestMapping(value = "serviceA",method = RequestMethod.GET) public Map<String,Object> serviceA () { Map<String,Object> map=new HashMap <>(); goodsBiz.goodsInfo(); map.put("code" ,200 ); return map; } @RequestMapping(value = "serviceB",method = RequestMethod.GET) public Map<String,Object> serviceB () { Map<String,Object> map=new HashMap <>(); goodsBiz.goodsInfo(); map.put("code" ,200 ); return map; } @Service public class GoodsBiz { @SentinelResource("goodsInfo") public void goodsInfo () { System.out.println("商品信息" ); } }
测试
流控效果 直接失败
Warm Up
匀速排队
熔断降级
参考官方文档熔断降级 · alibaba/Sentinel Wiki (github.com)
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。为了防止整个链路不可用,我们需要对不稳定的弱依赖服务调用
进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩
慢调用比列
@GetMapping("payAction") public Map<String,Object> payAction (Integer flag) throws InterruptedException { if (flag==null ){ Thread.sleep(3000 ); } Map<String, Object> map = new HashMap <>(); map.put("code" ,200 ); return map; }
增加熔断规则
注意:这里让每个请求睡3秒,熔断时长为两秒,压力测试时,请求不可太快,否则会没有效果,因为是模拟测试,测试没达到预期时,修改请求发送时间和熔断时间
测试结果
这里是在5秒内发100个请求,就是一秒20个超过了最小请求数,而每个线程都睡3秒肯定超过了慢调用的阈值和比列,所以系统进行熔断,经过两秒后(设置的熔断时长)发送一个请求过去看系统是否可用,发现不可用后继续熔断
异常比列
当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。
异常数
当单位统计时长内的异常数目超过阈值之后会自动进行熔断。
异常数只能是业务异常,主动抛异常模拟测试
@GetMapping("payAction") public Map<String,Object> payAction (Integer flag) throws InterruptedException { Random r=new Random (); int a= r.nextInt(5 ); if (a==2 ||a==4 ){ throw new RuntimeException ("我要异常咯" ); } Map<String, Object> map = new HashMap <>(); map.put("code" ,200 ); return map; }
在这个统计时长内异常数超过6就熔断
结果如下
熔断尝试关闭放一个请求过去,通过的请求数增加
热点限流
热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。
尽管controller会自动设置资源名,但是对于热点流,要自己设置资源名,在一个热点资源上加@SentinelResource("hotkey-page")
测试
现在将分页查作为热点,在一秒内访问第一页的请求超过5个就熔断
流控回调 场景
如果流控规则起作用了,限制了一些请求,sentinel以 http响应码 4xx/5xx形式回送信息. 被限制的请求返回给用户的信息对用户并不友好,用户在使用对应服务时,不知道服务发生了什么导致不可用,所以可以用流控回调处理返回给用户的信息
解决方案blockHandler
blockHandler 用于处理被 Sentinel 阻止的请求,例如当请求超过限流阈值时,Sentinel 会自动阻止该请求,并调用指定的 blockHandler 方法进行处理 , 这样可以将流控这种异常信息转为业务要处理的消息格式.
在@SentinelResource资源时 加入回调方法
回调的函数要和对应资源的参数类型保持一致
例如在hotkey-page这个资源上加流控回调
@SentinelResource(value = "hotkey-page",blockHandler = "handleBlock") @RequestMapping(value = "/findByPage",method = {RequestMethod.POST,RequestMethod.GET}) public Map<String,Object> findByPage (@RequestParam int pageno,@RequestParam int pagesize, @RequestParam String sortby,@RequestParam String sort) public Map<String, Object> handleBlock (int pageno, int pagesize, String sortby, String sort , BlockException exception) { exception.printStackTrace(); Map<String, Object> map = new HashMap <>(); map.put("code" , 0 ); String info = "exception:" +exception.getMessage()+", rule:" +exception.getRule(); map.put("msg" ,info); return map;
为这个资源设置阈值为1 发10个请求看效果
没有阈值回调前,返回的请求
可根据自己的实际情况去返回相应的错误信息
fallback
出现业务异常,sentinel的默认方案是 以http 500码回送一条信息,用户同样不知道请求的服务出现了什么问题,对用户不友好。 fallback 则用于处理方法执行过程中的异常,例如当方法抛出异常时,Sentinel 会自动调用指定的 fallback 方法进行处理。
使用和上面blockHandler 步骤一致,只需注意异常类型为Throwable ,Throwable是所有异常的根类在测试时可通过手动关闭数据库服务来模拟义务异常
统一异常管理
上述两种情况是对单个资源进行处理,如果需要处理的资源很多,会加大我们编码的复杂度,所以可以直接对controller中出现的异常统一管理
利用spring boot的全局异常处理机制集中式处理异常
@ControllerAdvice注解采用aop机制处理controller中的异常
@ExceptionHandler(RuntimeException.class)捕获某种异常类型
@ResponseBody返回json响应
新增配置类如下
package com.y.config;import org.springframework.core.annotation.Order;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;import java.util.HashMap;import java.util.Map;@ControllerAdvice @Order(-100000) public class CustomerExceptionHandlerAdvice { public class CustomerExceptionHandler { @ExceptionHandler(RuntimeException.class) @ResponseBody public Map<String,Object> handleRuntimeException (RuntimeException exception) { Map<String,Object> map=new HashMap <>(); map.put("code" ,0 ); map.put("msg" ,"runtimeException occured" +exception.getMessage()); return map; } } }
@ExceptionHandler 对那个异常处理
新增这个配置类后,可以做到统一管理
测试,为了防止之前的blockHander和fallback的干扰,先将这两个配置去掉
关闭数据库连接模拟业务异常,返回给用户的还是200状态码,测试结果如下
这个不能处理流控异常因为请求是由sentinel直接处理的,请求没到控制层,这是对业务层的处理
对流控异常统一处理
利用springmvc的全局处理异常机制集中处理
把流控异常抛出到外面统一由MySentinelExceptionHandle 处理
引入依赖
<dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-spring-webmvc- adapter</artifactId > </dependency > <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-parameter-flow-control</artifactId > </dependency >
package com.y.config; import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;import com.alibaba.csp.sentinel.slots.block.BlockException;import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;import com.alibaba.csp.sentinel.slots.block.flow.FlowException;import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;import com.alibaba.csp.sentinel.slots.system.SystemBlockException;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.stereotype.Component;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.PrintWriter;import java.util.HashMap;import java.util.Map;@Component public class MySentinelExceptionHandler implements BlockExceptionHandler { @Override public void handle (HttpServletRequest request, HttpServletResponse response, BlockException ex) throws Exception { String msg = null ; if (ex instanceof FlowException) { msg = "访问频繁,请稍候再试" ; } else if (ex instanceof DegradeException) { msg = "系统降级" ; }else if ( ex instanceof ParamFlowException){ msg="热点参数异常:" + ex.getMessage()+"," +((ParamFlowException) ex).getResourceName()+"," +ex.getRule() ; } else if (ex instanceof SystemBlockException) { msg = "系统规则限流或降级" ; } else if (ex instanceof AuthorityException) { msg = "授权规则不通过" ; } else { msg = "未知限流降级" ; } response.setStatus(200 ); response.setCharacterEncoding("utf-8" ); response.setHeader("Content-Type" , "application/json;charset=utf-8" ); response.setContentType("application/json;charset=utf-8" ); Map map=new HashMap (); map.put("code" ,0 ); map. put("msg" , msg); ObjectMapper om=new ObjectMapper (); String json=om.writeValueAsString( map ); PrintWriter writer=response.getWriter(); writer.write( json ); writer.flush(); } }
为什么要request和response 因为sentinel异常是在有请求过来发生的,请求过来会被sentinel 拦截 sentinel底层会统计流控信息
block异常有很多子类,根据不同子类返回相应信息
异常信息会送前台需要输出流,输出流从responce中取
测试流控异常,看是否起作用 将阈值设置为1 结果如下