点赞再看,养成习惯,公众号搜一搜【一角钱技术】关注更多原创技术文章。本文 GitHub org_hejianhui/JavaStudy 已收录,有个人系列文章。html
业务场景,高并发调用java
简单来讲就是超时机制,配置如下超时时间,假如1秒——每次请求在1秒内必须返回,不然到点就把线程掐死,释放资源!git
思路:一旦超时,就释放资源。因为释放资源速度较快,应用就不会那么容易被拖死。github
代码演示:(针对调用方处理)web
// 第一步:设置RestTemplate的超时时间
@Configuration
public class WebConfig {
@Bean
public RestTemplate restTemplate() {
//设置restTemplate的超时时间
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setReadTimeout(1000);
requestFactory.setConnectTimeout(1000);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}
}
// 第二步:进行超时异常处理
try{
ResponseEntity<ProductInfo> responseEntity= restTemplate.getForEntity(uri+orderInfo.getProductNo(), ProductInfo.class);
productInfo = responseEntity.getBody();
}catch (Exception e) {
log.info("调用超时");
throw new RuntimeException("调用超时");
}
// 设置全局异常处理
@ControllerAdvice
public class NiuhExceptionHandler {
@ExceptionHandler(value = {RuntimeException.class})
@ResponseBody
public Object dealBizException() {
OrderVo orderVo = new OrderVo();
orderVo.setOrderNo("-1");
orderVo.setUserName("容错用户");
return orderVo;
}
}
复制代码
有兴趣能够先了解一下船舱构造——通常来讲,现代的轮船都会分不少舱室,舱室直接用钢板焊死,彼此隔离。这样即便有某个/某些船舱进水,也不会营销其它舱室,浮力够,船不会沉。spring
代码中的舱壁隔离(线程池隔离模式)json
M类使用线程池1,N类使用线程池2,彼此的线程池不一样,而且为每一个类分配的线程池大小,例如 coreSIze=10。api
举例子:M类调用B服务,N类调用C服务,若是M类和N类使用相同的线程池,那么若是B服务挂了,N类调用B服务的接口并发又很高,你又没有任何保护措施,你的服务就极可能被M类拖死。而若是M类有本身的线程池,N类也有本身的线程池,若是B服务挂了,M类顶可能是将本身的线程池占满,不会影响N类的线程池——因而N类依然能正常工做。缓存
思路:不把鸡蛋放在一个篮子里,你有你的线程池,我有个人线程池,你的线程池满类和我也不要紧,你挂了也和我也不要紧。服务器
现实世界的断路器你们确定都很了解,每一个人家里都会有断路器。断路器实时监控电路的状况,若是发现电路电流异常,就会跳闸,从而防止电路被烧毁。
软件世界的断路器能够这样理解:实时监测应用,若是发如今必定时间内失败次数/失败率达到必定阀值,就“跳闸”,断路器打开——次数,请求直接返回,而不去调用本来调用的逻辑。
跳闸一段时间后(例如15秒),断路器会进入半开状态,这是一个瞬间态,此时容许一个请求调用该调的逻辑,若是成功,则断路器关闭,应用正常调用;若是调用依然不成功,断路器继续回到打开状态,过段时间再进入半开状态尝试——经过“跳闸”,应用能够保护本身,并且避免资源浪费;而经过半开的设计,能够实现应用的“自我修复”
A lightweight powerful flow control component enabling reliability and monitoring for microservices.(轻量级的流量控制、熔断降级 Java 库) github官网地址:github.com/alibaba/Sen… wiki:github.com/alibaba/Sen…
Hystrix 在 Sentinel 面前就是弟弟
niuh04-ms-alibaba-sentinel-helloworld
V1版本:
<!--导入Sentinel的相关jar包-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.7.1</version>
</dependency>
复制代码
@RestController
@Slf4j
public class HelloWorldSentinelController {
@Autowired
private BusiServiceImpl busiService;
/** * 初始化流控规则 */
@PostConstruct
public void init() {
List<FlowRule> flowRules = new ArrayList<>();
/** * 定义 helloSentinelV1 受保护的资源的规则 */
//建立流控规则对象
FlowRule flowRule = new FlowRule();
//设置流控规则 QPS
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置受保护的资源
flowRule.setResource("helloSentinelV1");
//设置受保护的资源的阈值
flowRule.setCount(1);
flowRules.add(flowRule);
//加载配置好的规则
FlowRuleManager.loadRules(flowRules);
}
/** * 频繁请求接口 http://localhost:8080/helloSentinelV1 * 这种作法的缺点: * 1)业务侵入性很大,须要在你的controoler中写入 非业务代码.. * 2)配置不灵活 若须要添加新的受保护资源 须要手动添加 init方法来添加流控规则 * @return */
@RequestMapping("/helloSentinelV1")
public String testHelloSentinelV1() {
Entry entity =null;
//关联受保护的资源
try {
entity = SphU.entry("helloSentinelV1");
//开始执行 本身的业务方法
busiService.doBusi();
//结束执行本身的业务方法
} catch (BlockException e) {
log.info("testHelloSentinelV1方法被流控了");
return "testHelloSentinelV1方法被流控了";
}finally {
if(entity!=null) {
entity.exit();
}
}
return "OK";
}
}
复制代码
测试效果:http://localhost:8080/helloSentinelV1 V1版本的缺陷以下:
V2版本:基于V1版本,再添加一个依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>1.7.1</version>
</dependency>
复制代码
// 配置一个切面
@Configuration
public class SentinelConfig {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
/** * 初始化流控规则 */
@PostConstruct
public void init() {
List<FlowRule> flowRules = new ArrayList<>();
/** * 定义 helloSentinelV2 受保护的资源的规则 */
//建立流控规则对象
FlowRule flowRule2 = new FlowRule();
//设置流控规则 QPS
flowRule2.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置受保护的资源
flowRule2.setResource("helloSentinelV2");
//设置受保护的资源的阈值
flowRule2.setCount(1);
flowRules.add(flowRule2);
}
/** * 频繁请求接口 http://localhost:8080/helloSentinelV2 * 优势: 须要配置aspectj的切面SentinelResourceAspect ,添加注解@SentinelResource * 解决了v1版本中 sentinel的业务侵入代码问题,经过blockHandler指定被流控后调用的方法. * 缺点: 若咱们的controller中的方法逐步变多,那么受保护的方法也愈来愈多,会致使一个问题 * blockHandler的方法也会愈来愈多 引发方法急剧膨胀 怎么解决 * * 注意点: * blockHandler 对应处理 BlockException 的函数名称, * 可选项。blockHandler 函数访问范围须要是 public,返回类型须要与原方法相匹配, * 参数类型须要和原方法相匹配而且最后加一个额外的参数, * 类型为 BlockException。blockHandler 函数默认须要和原方法在同一个类中 * @return */
@RequestMapping("/helloSentinelV2")
@SentinelResource(value = "helloSentinelV2",blockHandler ="testHelloSentinelV2BlockMethod")
public String testHelloSentinelV2() {
busiService.doBusi();
return "OK";
}
public String testHelloSentinelV2BlockMethod(BlockException e) {
log.info("testRt流控");
return "testRt降级 流控...."+e;
}
复制代码
测试效果:http://localhost:8080/helloSentinelV2
V3版本 基于V2缺点改进
/** * 初始化流控规则 */
@PostConstruct
public void init() {
List<FlowRule> flowRules = new ArrayList<>();
/** * 定义 helloSentinelV3 受保护的资源的规则 */
//建立流控规则对象
FlowRule flowRule3 = new FlowRule();
//设置流控规则 QPS
flowRule3.setGrade(RuleConstant.FLOW_GRADE_QPS);
//设置受保护的资源
flowRule3.setResource("helloSentinelV3");
//设置受保护的资源的阈值
flowRule3.setCount(1);
flowRules.add(flowRule3);
}
/** * 咱们看到了v2中的缺点,咱们经过blockHandlerClass 来指定处理被流控的类 * 经过testHelloSentinelV3BlockMethod 来指定blockHandlerClass 中的方法名称 * ***这种方式 处理异常流控的方法必需要是static的 * 频繁请求接口 http://localhost:8080/helloSentinelV3 * @return */
@RequestMapping("/helloSentinelV3")
@SentinelResource(value = "helloSentinelV3",blockHandler = "testHelloSentinelV3BlockMethod",blockHandlerClass = BlockUtils.class)
public String testHelloSentinelV3() {
busiService.doBusi();
return "OK";
}
// 异常处理类
@Slf4j
public class BlockUtils {
public static String testHelloSentinelV3BlockMethod(BlockException e){
log.info("testHelloSentinelV3方法被流控了");
return "testHelloSentinelV3方法被流控了";
}
}
复制代码
测试效果:http://localhost:8080/helloSentinelV3 缺点:不能动态的添加规则。如何解决问题?
<!--加入sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--加入actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
复制代码
添加Sentinel后,会暴露/actuator/sentinel 端点http://localhost:8080/actuator/sentinel
而Springboot默认是没有暴露该端点的,因此咱们须要本身配置
server:
port: 8080
management:
endpoints:
web:
exposure:
include: '*'
复制代码
下载地址:github.com/alibaba/Sen… (我这里版本是:1.6.3)
java -jar sentinel-dashboard-1.6.3.jar
启动(就是一个SpringBoot工程)spring:
cloud:
sentinel:
transport:
dashboard: localhost:9999
复制代码
在这个面板中咱们监控咱们接口的 经过的QPS 和 拒绝的QPS,在没有设置流控规则,咱们是看不到拒绝的QPS。
用来线上微服务的所监控的API
簇点链路 选择具体的访问的API,而后点击“流控按钮” 含义:
疯狂的请求这个路径
业务场景:咱们如今有两个API,第一个是保存订单,一个是查询订单,假设咱们但愿有限操做“保存订单” 测试:写两个读写测试接口
/** * 方法实现说明:模仿 流控模式【关联】 读接口 * @author:hejianhui * @param orderNo * @return: * @exception: * @date:2019/11/24 22:06 */
@RequestMapping("/findById/{orderNo}")
public Object findById(@PathVariable("orderNo") String orderNo) {
log.info("orderNo:{}","执行查询操做"+System.currentTimeMillis());
return orderInfoMapper.selectOrderInfoById(orderNo);
}
/** * 方法实现说明:模仿流控模式【关联】 写接口(优先) * @author:hejianhui * @return: * @exception: * @date:2019/11/24 22:07 */
@RequestMapping("/saveOrder")
public String saveOrder() throws InterruptedException {
//Thread.sleep(500);
log.info("执行保存操做,模仿返回订单ID");
return UUID.randomUUID().toString();
}
复制代码
测试代码:写一个for循环一直调用咱们的写接口,让写接口QPS达到阀值
public class TestSentinelRule {
public static void main(String[] args) throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
for(int i=0;i<1000;i++) {
restTemplate.postForObject("http://localhost:8080/saveOrder",null,String.class);
Thread.sleep(10);
}
}
}
复制代码
此时访问咱们的读接口:此时被限流了。
用法说明,本地实验没成功,用alibaba 未毕业版本0.9.0能够测试出效果,API级别的限制流量
代码:
@RequestMapping("/findAll")
public String findAll() throws InterruptedException {
orderServiceImpl.common();
return "findAll";
}
@RequestMapping("/findAllByCondtion")
public String findAllByCondtion() {
orderServiceImpl.common();
return "findAllByCondition";
}
@Service
public class OrderServiceImpl {
@SentinelResource("common")
public String common() {
return "common";
}
}
复制代码
根据流控规则来讲: 只会限制/findAll的请求,不会限制/findAllByCondtion规则
源码:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
源码:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController>
当流量忽然增大的时候,咱们经常会但愿系统从空闲状态到繁忙状态的切换的时间长一些。即若是系统在此以前长期处于空闲的状态,咱们但愿处理请求的数量是缓步增长,通过预期的时间后,到达系统处理请求个数的最大值。Warm Up (冷启动,预热)模式就是为了实现这个目的。
冷加载因子:codeFacotr 默认是3
上图设置:就是QPS从100/3=33开始算, 通过10秒钟,达到一百的QPS 才进行限制流量。
源码:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
这种方式适合用于请求以突刺状来到,这个时候咱们不但愿一会儿把全部的请求都经过,这样可能会把系统压垮;同时咱们也期待系统以稳定的速度,逐步处理这些请求,以起到“削峰填谷”的效果,而不是拒绝全部请求。
选择排队等待的阀值类型必须是****QPS 上图设置:单机阀值为10,表示每秒经过的请求个数是10,也就是每一个请求平均间隔恒定为 1000 / 10 = 100 ms,每个请求的最长等待时间(maxQueueingTimeMs)为 20 * 1000ms = 20s。,超过20s就丢弃请求。
平均响应时间(DEGRADE_GRADE_RT):当 1s 内持续进入5个请求,对应时刻的平均响应时间(秒级)均超过阀值(count,以 ms 为单位),那么在接下来的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)以内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。
注意:Sentinel 默认同级的 RT 上限是4900ms,超出此阀值都会算作4900ms,若须要变动此上限能够经过启动配置项:-Dcsp.sentinel.statistic.max.rt=xxx 来配置
当资源的每秒请求量 >= 5,而且每秒异常总数占经过量的比值超过阀值(DegradeRule 中的 count)以后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)以内,对这个方法的调用都会自动地返回。异常比例的阀值范围是 [0.0, 1.0],表明 0% ~ 100% 。
当资源近千分之的异常数目超过阀值以后会进行熔断。注意因为统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。
业务场景:秒杀业务,好比商场作促销秒杀,针对苹果11(商品id=1)进行9.9秒杀活动,那么这个时候,咱们去请求订单接口(商品id=1)的请求流量十分大,咱们就能够经过热点参数规则来控制 商品id=1 的请求的并发量。而其余正常商品的请求不会受到限制。那么这种热点参数规则使用。
咱们经过观察到sentinel-dashboard的机器列表上观察注册服务微服务信息。咱们的 控制台就能够经过这些微服务的注册信息跟咱们的具体的微服务进行通讯.
@RestController
public class AddFlowLimitController {
@RequestMapping("/addFlowLimit")
public String addFlowLimit() {
List<FlowRule> flowRuleList = new ArrayList<>();
FlowRule flowRule = new FlowRule("/testAddFlowLimitRule");
//设置QPS阈值
flowRule.setCount(1);
//设置流控模型为QPS模型
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
flowRuleList.add(flowRule);
FlowRuleManager.loadRules(flowRuleList);
return "success";
}
@RequestMapping("/testAddFlowLimitRule")
public String testAddFlowLimitRule() {
return "testAddFlowLimitRule";
}
}
复制代码
添加效果截图: 执行:http://localhost:8080/addFlowLimit Sentinel具体配置项:github.com/alibaba/Sen…
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:9999
filter:
enabled: true #关闭Spring mvc的端点保护
复制代码
那么咱们的这种类型的接口 不会被sentinel保护 只有加了
@SentinelResource
的注解的资源才会被保护
<!--加入ribbon-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<!--加入sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--加入actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
复制代码
在咱们的RestTemplate组件上添加@SentinelRestTemplate注解。而且咱们能够经过在@SentinelRestTemplate 一样的能够指定咱们的 blockHandlerClass、fallbackClass、blockHandler、fallback 这四个属性
@Configuration
public class WebConfig {
@Bean
@LoadBalanced
@SentinelRestTemplate( blockHandler = "handleException",blockHandlerClass = GlobalExceptionHandler.class, fallback = "fallback",fallbackClass = GlobalExceptionHandler.class )
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
*****************全局异常处理类*****************
@Slf4j
public class GlobalExceptionHandler {
/** * 限流后处理方法 * @param request * @param body * @param execution * @param ex * @return */
public static SentinelClientHttpResponse handleException(HttpRequest request, byte[] body, ClientHttpRequestExecution execution, BlockException ex) {
ProductInfo productInfo = new ProductInfo();
productInfo.setProductName("被限制流量拉");
productInfo.setProductNo("-1");
ObjectMapper objectMapper = new ObjectMapper();
try {
return new SentinelClientHttpResponse(objectMapper.writeValueAsString(productInfo));
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
/** * 熔断后处理的方法 * @param request * @param body * @param execution * @param ex * @return */
public static SentinelClientHttpResponse fallback(HttpRequest request, byte[] body, ClientHttpRequestExecution execution, BlockException ex) {
ProductInfo productInfo = new ProductInfo();
productInfo.setProductName("被降级拉");
productInfo.setProductNo("-1");
ObjectMapper objectMapper = new ObjectMapper();
try {
return new SentinelClientHttpResponse(objectMapper.writeValueAsString(productInfo));
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
}
复制代码
何时关闭:通常在咱们的本身测试业务功能是否正常的状况,关闭该配置
#是否开启@SentinelRestTemplate注解
resttemplate:
sentinel:
enabled: true
复制代码
在niuh05-ms-alibaba-feignwithsentinel-order上 pom.xml中添加配置
<!--加入sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--加入actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.niuh</groupId>
<artifactId>niuh03-ms-alibaba-feign-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
复制代码
@FeignClient(name = "product-center",fallback = ProductCenterFeignApiWithSentinelFallback.class)
public interface ProductCenterFeignApiWithSentinel {
/** * 声明式接口,远程调用http://product-center/selectProductInfoById/{productNo} * @param productNo * @return */
@RequestMapping("/selectProductInfoById/{productNo}")
ProductInfo selectProductInfoById(@PathVariable("productNo") String productNo) throws InterruptedException;
}
复制代码
咱们feign的限流降级接口(经过fallback没有办法获取到异常的)
@Component
public class ProductCenterFeignApiWithSentinelFallback implements ProductCenterFeignApiWithSentinel {
@Override
public ProductInfo selectProductInfoById(String productNo) {
ProductInfo productInfo = new ProductInfo();
productInfo.setProductName("默认商品");
return productInfo;
}
}
复制代码
package com.niuh.feignapi.sentinel;
import com.niuh.entity.ProductInfo;
import com.niuh.handler.ProductCenterFeignApiWithSentielFallbackFactoryasdasf;
import com.niuh.handler.ProductCenterFeignApiWithSentinelFallback;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/** * Created by hejianhui on 2019/11/22. */
@FeignClient(name = "product-center",fallbackFactory = ProductCenterFeignApiWithSentielFallbackFactoryasdasf.class)
public interface ProductCenterFeignApiWithSentinel {
/** * 声明式接口,远程调用http://product-center/selectProductInfoById/{productNo} * @param productNo * @return */
@RequestMapping("/selectProductInfoById/{productNo}")
ProductInfo selectProductInfoById(@PathVariable("productNo") String productNo) throws InterruptedException;
}
复制代码
经过FallbackFactory属性能够处理咱们的异常
@Component
@Slf4j
public class ProductCenterFeignApiWithSentielFallbackFactoryasdasf implements FallbackFactory<ProductCenterFeignApiWithSentinel> {
@Override
public ProductCenterFeignApiWithSentinel create(Throwable throwable) {
return new ProductCenterFeignApiWithSentinel(){
@Override
public ProductInfo selectProductInfoById(String productNo) {
ProductInfo productInfo = new ProductInfo();
if (throwable instanceof FlowException) {
log.error("流控了....{}",throwable.getMessage());
productInfo.setProductName("我是被流控的默认商品");
}else {
log.error("降级了....{}",throwable.getMessage());
productInfo.setProductName("我是被降级的默认商品");
}
return productInfo;
}
};
}
}
复制代码
Sentinel-dashboard 配置的规则,在咱们的微服务以及控制台重启的时候就清空了,由于它是基于内存的。
Dashboard 的推送规则方式是经过 API 将规则推送至客户端并直接更新到内存。 优缺点:这种作法的好处是简单,无依赖;坏处是应用重启规则就会消失,仅用于简单测试,不能用于生产环境。
首先 Sentinel 控制台经过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的规则保存到本地的文件中。使用 pull 模式的数据源时通常不须要对 Sentinel 控制台进行改造。
这种实现方法好处是简单,不引入新的依赖,坏处是没法保证监控数据的一致性
经过SPI扩展机制进行扩展,咱们写一个拉模式的实现类 com.niuh.persistence.PullModeByFileDataSource ,而后在工厂目录下建立 META-INF/services/com.alibaba.csp.sentinel.init.InitFun文件。 文件的内容就是写咱们的拉模式的实现类:
代码在niuh05-ms-alibaba-sentinelrulepersistencepull-order 工程的persistence包下。
微服务改造方案
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
复制代码
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:9999
#namespace: bc7613d2-2e22-4292-a748-48b78170f14c #指定namespace的id
datasource:
# 名称随意
flow:
nacos:
server-addr: 47.111.191.111:8848
dataId: ${spring.application.name}-flow-rules
groupId: SENTINEL_GROUP
rule-type: flow
degrade:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-degrade-rules
groupId: SENTINEL_GROUP
rule-type: degrade
system:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-system-rules
groupId: SENTINEL_GROUP
rule-type: system
authority:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-authority-rules
groupId: SENTINEL_GROUP
rule-type: authority
param-flow:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-param-flow-rules
groupId: SENTINEL_GROUP
rule-type: param-flow
复制代码
Sentinel-dashboard改造方案
<!-- for Nacos rule publisher sample -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<!-- <scope>test</scope>--> // 须要把test注释掉
</dependency>
复制代码
控制台改造主要是为规则实现:
在sentinel-dashboard工程目录com.alibaba.csp.sentinel.dashboard.rule 下建立一 个Nacos的包,而后把咱们的各个场景的配置规则类写到该包下.
咱们以ParamFlowRuleController(热点参数流控类做为修改做为演示)
/** * @author Eric Zhao * @since 0.2.1 */
@RestController
@RequestMapping(value = "/paramFlow")
public class ParamFlowRuleController {
private final Logger logger = LoggerFactory.getLogger(ParamFlowRuleController.class);
@Autowired
private SentinelApiClient sentinelApiClient;
@Autowired
private AppManagement appManagement;
@Autowired
private RuleRepository<ParamFlowRuleEntity, Long> repository;
@Autowired
@Qualifier("niuhHotParamFlowRuleNacosPublisher")
private DynamicRulePublisher<List<ParamFlowRuleEntity>> rulePublisher;
@Autowired
@Qualifier("niuhHotParamFlowRuleNacosProvider")
private DynamicRuleProvider<List<ParamFlowRuleEntity>> ruleProvider;
@Autowired
private AuthService<HttpServletRequest> authService;
private boolean checkIfSupported(String app, String ip, int port) {
try {
return Optional.ofNullable(appManagement.getDetailApp(app))
.flatMap(e -> e.getMachine(ip, port))
.flatMap(m -> VersionUtils.parseVersion(m.getVersion())
.map(v -> v.greaterOrEqual(version020)))
.orElse(true);
// If error occurred or cannot retrieve machine info, return true.
} catch (Exception ex) {
return true;
}
}
@GetMapping("/rules")
public Result<List<ParamFlowRuleEntity>> apiQueryAllRulesForMachine(HttpServletRequest request,
@RequestParam String app,
@RequestParam String ip,
@RequestParam Integer port) {
AuthUser authUser = authService.getAuthUser(request);
authUser.authTarget(app, PrivilegeType.READ_RULE);
if (StringUtil.isEmpty(app)) {
return Result.ofFail(-1, "app cannot be null or empty");
}
if (StringUtil.isEmpty(ip)) {
return Result.ofFail(-1, "ip cannot be null or empty");
}
if (port == null || port <= 0) {
return Result.ofFail(-1, "Invalid parameter: port");
}
if (!checkIfSupported(app, ip, port)) {
return unsupportedVersion();
}
try {
/* return sentinelApiClient.fetchParamFlowRulesOfMachine(app, ip, port) .thenApply(repository::saveAll) .thenApply(Result::ofSuccess) .get();*/
List<ParamFlowRuleEntity> rules = ruleProvider.getRules(app);
rules = repository.saveAll(rules);
return Result.ofSuccess(rules);
} catch (ExecutionException ex) {
logger.error("Error when querying parameter flow rules", ex.getCause());
if (isNotSupported(ex.getCause())) {
return unsupportedVersion();
} else {
return Result.ofThrowable(-1, ex.getCause());
}
} catch (Throwable throwable) {
logger.error("Error when querying parameter flow rules", throwable);
return Result.ofFail(-1, throwable.getMessage());
}
}
private boolean isNotSupported(Throwable ex) {
return ex instanceof CommandNotFoundException;
}
@PostMapping("/rule")
public Result<ParamFlowRuleEntity> apiAddParamFlowRule(HttpServletRequest request, @RequestBody ParamFlowRuleEntity entity) {
AuthUser authUser = authService.getAuthUser(request);
authUser.authTarget(entity.getApp(), PrivilegeType.WRITE_RULE);
Result<ParamFlowRuleEntity> checkResult = checkEntityInternal(entity);
if (checkResult != null) {
return checkResult;
}
if (!checkIfSupported(entity.getApp(), entity.getIp(), entity.getPort())) {
return unsupportedVersion();
}
entity.setId(null);
entity.getRule().setResource(entity.getResource().trim());
Date date = new Date();
entity.setGmtCreate(date);
entity.setGmtModified(date);
try {
entity = repository.save(entity);
//publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get();
publishRules(entity.getApp());
return Result.ofSuccess(entity);
} catch (ExecutionException ex) {
logger.error("Error when adding new parameter flow rules", ex.getCause());
if (isNotSupported(ex.getCause())) {
return unsupportedVersion();
} else {
return Result.ofThrowable(-1, ex.getCause());
}
} catch (Throwable throwable) {
logger.error("Error when adding new parameter flow rules", throwable);
return Result.ofFail(-1, throwable.getMessage());
}
}
private <R> Result<R> checkEntityInternal(ParamFlowRuleEntity entity) {
if (entity == null) {
return Result.ofFail(-1, "bad rule body");
}
if (StringUtil.isBlank(entity.getApp())) {
return Result.ofFail(-1, "app can't be null or empty");
}
if (StringUtil.isBlank(entity.getIp())) {
return Result.ofFail(-1, "ip can't be null or empty");
}
if (entity.getPort() == null || entity.getPort() <= 0) {
return Result.ofFail(-1, "port can't be null");
}
if (entity.getRule() == null) {
return Result.ofFail(-1, "rule can't be null");
}
if (StringUtil.isBlank(entity.getResource())) {
return Result.ofFail(-1, "resource name cannot be null or empty");
}
if (entity.getCount() < 0) {
return Result.ofFail(-1, "count should be valid");
}
if (entity.getGrade() != RuleConstant.FLOW_GRADE_QPS) {
return Result.ofFail(-1, "Unknown mode (blockGrade) for parameter flow control");
}
if (entity.getParamIdx() == null || entity.getParamIdx() < 0) {
return Result.ofFail(-1, "paramIdx should be valid");
}
if (entity.getDurationInSec() <= 0) {
return Result.ofFail(-1, "durationInSec should be valid");
}
if (entity.getControlBehavior() < 0) {
return Result.ofFail(-1, "controlBehavior should be valid");
}
return null;
}
@PutMapping("/rule/{id}")
public Result<ParamFlowRuleEntity> apiUpdateParamFlowRule(HttpServletRequest request, @PathVariable("id") Long id, @RequestBody ParamFlowRuleEntity entity) {
AuthUser authUser = authService.getAuthUser(request);
if (id == null || id <= 0) {
return Result.ofFail(-1, "Invalid id");
}
ParamFlowRuleEntity oldEntity = repository.findById(id);
if (oldEntity == null) {
return Result.ofFail(-1, "id " + id + " does not exist");
}
authUser.authTarget(oldEntity.getApp(), PrivilegeType.WRITE_RULE);
Result<ParamFlowRuleEntity> checkResult = checkEntityInternal(entity);
if (checkResult != null) {
return checkResult;
}
if (!checkIfSupported(entity.getApp(), entity.getIp(), entity.getPort())) {
return unsupportedVersion();
}
entity.setId(id);
Date date = new Date();
entity.setGmtCreate(oldEntity.getGmtCreate());
entity.setGmtModified(date);
try {
entity = repository.save(entity);
//publishRules(entity.getApp(), entity.getIp(), entity.getPort()).get();
publishRules(entity.getApp());
return Result.ofSuccess(entity);
} catch (ExecutionException ex) {
logger.error("Error when updating parameter flow rules, id=" + id, ex.getCause());
if (isNotSupported(ex.getCause())) {
return unsupportedVersion();
} else {
return Result.ofThrowable(-1, ex.getCause());
}
} catch (Throwable throwable) {
logger.error("Error when updating parameter flow rules, id=" + id, throwable);
return Result.ofFail(-1, throwable.getMessage());
}
}
@DeleteMapping("/rule/{id}")
public Result<Long> apiDeleteRule(HttpServletRequest request, @PathVariable("id") Long id) {
AuthUser authUser = authService.getAuthUser(request);
if (id == null) {
return Result.ofFail(-1, "id cannot be null");
}
ParamFlowRuleEntity oldEntity = repository.findById(id);
if (oldEntity == null) {
return Result.ofSuccess(null);
}
authUser.authTarget(oldEntity.getApp(), PrivilegeType.DELETE_RULE);
try {
repository.delete(id);
/*publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort()).get();*/
publishRules(oldEntity.getApp());
return Result.ofSuccess(id);
} catch (ExecutionException ex) {
logger.error("Error when deleting parameter flow rules", ex.getCause());
if (isNotSupported(ex.getCause())) {
return unsupportedVersion();
} else {
return Result.ofThrowable(-1, ex.getCause());
}
} catch (Throwable throwable) {
logger.error("Error when deleting parameter flow rules", throwable);
return Result.ofFail(-1, throwable.getMessage());
}
}
private CompletableFuture<Void> publishRules(String app, String ip, Integer port) {
List<ParamFlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
return sentinelApiClient.setParamFlowRuleOfMachine(app, ip, port, rules);
}
private void publishRules(String app) throws Exception {
List<ParamFlowRuleEntity> rules = repository.findAllByApp(app);
rulePublisher.publish(app, rules);
}
private <R> Result<R> unsupportedVersion() {
return Result.ofFail(4041,
"Sentinel client not supported for parameter flow control (unsupported version or dependency absent)");
}
private final SentinelVersion version020 = new SentinelVersion().setMinorVersion(2);
}
复制代码
第一步:访问 help.aliyun.com/document_de… 第二步:免费开通
第三步:开通
第四步:接入应用
第五步:点击接入SDK
第六步:加入咱们的应用
以niuh05-ms-alibaba-sentinelrulepersistence-ahas-order工程为例
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>spring‐boot‐starter‐ahas‐sentinel‐client</artifactId> 4 <version>1.5.0</version>
</dependency>
复制代码
ahas.namespace: default
project.name: order-center
ahas.license: b833de8ab5f34e4686457ecb2b60fa46
复制代码
@SentinelResource("hot-param-flow-rule")
@RequestMapping("/testHotParamFlowRule")
public OrderInfo testHotParamFlowRule(@RequestParam("orderNo") String orderNo) {
return orderInfoMapper.selectOrderInfoById(orderNo);
}
复制代码
第一次访问接口: AHas控制台出现咱们的微服务
添加咱们直接的流控规则
疯狂刷新咱们的测试接口:
发现这两种错误都是医院,显然这里咱们须要优化
UrlBlockHandler
提供了一个接口,咱们须要实现这个接口
/** * @vlog: 高于生活,源于生活 * @desc: 类的描述:处理流控,降级规则 * @author: hejianhui * @createDate: 2019/12/3 16:40 * @version: 1.0 */
@Component
public class NiuhUrlBlockHandler implements UrlBlockHandler {
public static final Logger log = LoggerFactory.getLogger(NiuhUrlBlockHandler.class);
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
if(ex instanceof FlowException) {
log.warn("触发了流控");
warrperResponse(response,ErrorEnum.FLOW_RULE_ERR);
}else if(ex instanceof ParamFlowException) {
log.warn("触发了参数流控");
warrperResponse(response,ErrorEnum.HOT_PARAM_FLOW_RULE_ERR);
}else if(ex instanceof AuthorityException) {
log.warn("触发了受权规则");
warrperResponse(response,ErrorEnum.AUTH_RULE_ERR);
}else if(ex instanceof SystemBlockException) {
log.warn("触发了系统规则");
warrperResponse(response,ErrorEnum.SYS_RULE_ERR);
}else{
log.warn("触发了降级规则");
warrperResponse(response,ErrorEnum.DEGRADE_RULE_ERR);
}
}
private void warrperResponse(HttpServletResponse httpServletResponse, ErrorEnum errorEnum) throws IOException {
httpServletResponse.setStatus(500);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8");
httpServletResponse.setContentType("application/json;charset=utf-8");
ObjectMapper objectMapper = new ObjectMapper();
String errMsg =objectMapper.writeValueAsString(new ErrorResult(errorEnum));
httpServletResponse.getWriter().write(errMsg);
}
}
复制代码
优化后:
Sentinel 提供了一个
RequestOriginParser
接口,咱们能够在这里实现编码从请求头中区分来源
/** * @vlog: 高于生活,源于生活 * @desc: 类的描述:区分来源接口 * @author: hejianhui * @createDate: 2019/12/4 13:13 * @version: 1.0 */
/*@Component*/
@Slf4j
public class NiuhRequestOriginParse implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
String origin = request.getHeader("origin");
if(StringUtils.isEmpty(origin)) {
log.warn("origin must not null");
throw new IllegalArgumentException("request origin must not null");
}
return origin;
}
}
复制代码
配置设置区分来源为:yijiaoqian
例如:/selectOrderInfoById/2 、 /selectOrderInfoById/1 须要转为/selectOrderInfoById/{number}
/** * @vlog: 高于生活,源于生活 * @desc: 类的描述:解决RestFule风格的请求 * eg: /selectOrderInfoById/2 /selectOrderInfoById/1 须要转为/selectOrderInfoById/{number} * @author: hejianhui * @createDate: 2019/12/4 13:28 * @version: 1.0 */
@Component
@Slf4j
public class NiuhUrlClean implements UrlCleaner {
@Override
public String clean(String originUrl) {
log.info("originUrl:{}",originUrl);
if(StringUtils.isEmpty(originUrl)) {
log.error("originUrl not be null");
throw new IllegalArgumentException("originUrl not be null");
}
return replaceRestfulUrl(originUrl);
}
/** * 方法实现说明:把/selectOrderInfoById/2 替换成/selectOrderInfoById/{number} * @author:hejianhui * @param sourceUrl 目标url * @return: 替换后的url * @exception: * @date:2019/12/4 13:46 */
private String replaceRestfulUrl(String sourceUrl) {
List<String> origins = Arrays.asList(sourceUrl.split("/"));
StringBuffer targetUrl = new StringBuffer("/");
for(String str:origins) {
if(NumberUtils.isNumber(str)) {
targetUrl.append("/{number}");
}else {
targetUrl.append(str);
}
}
return targetUrl.toString();
}
}
复制代码
PS:以上代码提交在 Github :github.com/Niuh-Study/…
文章持续更新,能够公众号搜一搜「 一角钱技术 」第一时间阅读, 本文 GitHub org_hejianhui/JavaStudy 已经收录,欢迎 Star。