@SentinelResource详解
Sentinel需要先把可能需要保护的资源定义好,之后再配置规则。也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。主流框架的默认适配抛出异常的方式定义资源返回布尔值方式定义资源注解方式定义资源异步调用支持这里我不再一一的详细介绍,想要详细了解每种方式的同学可以自行查阅官网。
往期回顾
前面我们已经简单的介绍了如何安装与使用Sentinel,接下来我们继续来看看Sentinel的一些核心概念以及特性吧
基本概念回顾
我们前面已经介绍过Sentinel的两个核心概念,这里我们再回顾一下吧:
- Sentinel 的基本概念有两个,它们分别是:资源和规则
基本概念 | 描述 |
---|---|
资源 | 资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如由应用程序提供的服务或者是服务里的方法,甚至可以是一段代码。 我们可以通过 Sentinel 提供的 API 来定义一个资源,使其能够被 Sentinel 保护起来。通常情况下,我们可以使用方法名、URL 甚至是服务名来作为资源名来描述某个资源。 |
规则 | 围绕资源而设定的规则。Sentinel 支持流量控制、熔断降级、系统保护、来源访问控制和热点参数等多种规则,所有这些规则都可以动态实时调整。 |
定义资源
Sentinel需要先把可能需要保护的资源定义好,之后再配置规则。也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。
Sentinel提供多种定义资源的方式,分别是:
- 主流框架的默认适配
- 抛出异常的方式定义资源
- 返回布尔值方式定义资源
- 注解方式定义资源
- 异步调用支持
这里我不再一一的详细介绍,想要详细了解每种方式的同学可以自行查阅官网
@SentinelResource 注解
Sentinel 支持通过 @SentinelResource
注解定义资源并配置 blockHandler
和 fallback
函数来进行限流之后的处理。@SentinelResource 注解是 Sentinel 提供的最重要的注解之一,它还包含了多个属性,如下表:
属性 | 说明 | 必填与否 | 使用要求 |
---|---|---|---|
value | 用于指定资源的名称 | 必填 | - |
entryType | entry 类型 | 可选项(默认为 EntryType.OUT) | - |
blockHandler | 服务限流后会抛出 BlockException 异常,而 blockHandler 则是用来指定一个函数来处理 BlockException 异常的。 简单点说,该属性用于指定服务限流后的后续处理逻辑。 | 可选项 |
|
blockHandlerClass | 若 blockHandler 函数与原方法不在同一个类中,则需要使用该属性指定 blockHandler 函数所在的类。 | 可选项 |
|
fallback | 用于在抛出异常(包括 BlockException)时,提供 fallback 处理逻辑。 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。 | 可选项 |
|
fallbackClass | 若 fallback 函数与原方法不在同一个类中,则需要使用该属性指定 blockHandler 函数所在的类。 | 可选项 |
|
defaultFallback | 默认的 fallback 函数名称,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。 默认 fallback 函数可以针对所以类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。 | 可选项 |
|
exceptionsToIgnore | 用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。 | 可选项 | - |
注:在 Sentinel 1.6.0 之前,fallback 函数只针对降级异常(DegradeException)进行处理,不能处理业务异常。
让我们再回顾一下之前的例子
@GetMapping("/register")
@SentinelResource("register")
public CommonResult<String> register() {
userService.register();
return ResultUtils.success();
}
前面我们用到的@SentinelResource注解就是定义资源了,服务启动并被调用过该接口后(Sentinel懒加载)就可以再控制台看到对应的资源,我们就可以对其添加对应的规则了
注意:注解方式埋点不支持 private 方法。
特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出
BlockException
时只会进入blockHandler
处理逻辑。若未配置blockHandler
、fallback
和defaultFallback
,则被限流降级时会将BlockException
直接抛出。
代码示例:
public class TestService {
// 对应的 `handleException` 函数需要位于 `ExceptionUtil` 类中,并且必须为 static 函数.
@SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
public void test() {
System.out.println("Test");
}
// 原函数
@SentinelResource(value = "hello", blockHandler = "exceptionHandler", fallback = "helloFallback")
public String hello(long s) {
return String.format("Hello at %d", s);
}
// Fallback 函数,函数签名与原函数一致或加一个 Throwable 类型的参数.
public String helloFallback(long s) {
return String.format("Halooooo %d", s);
}
// Block 异常处理函数,参数最后多一个 BlockException,其余与原函数一致.
public String exceptionHandler(long s, BlockException ex) {
// Do some log here.
ex.printStackTrace();
return "Oops, error occurred at " + s;
}
}
Sentinel自定义限流异常处理
系统有默认的异常页面,但是该异常页面对用户不友好,我们应该尽量避免直接返回系统默认的异常页面,而需要根据自己的需求自定义对应的异常处理逻辑
方法级别
我们可以自定义通用的限流处理逻辑,然后在@SentinelResource中指定。
/**
* @author Pymjl
* @version 1.0
* @date 2022/8/25 12:48
**/
@RestController
@RequestMapping("/user")
@Log4j2
public class UserController {
@Resource
UserService userService;
@Value("${server.port}")
private String port;
@GetMapping("/test")
@SentinelResource(value = "test", blockHandler = "handleTest")
public CommonResult<String> test(HttpServletRequest request) throws UnknownHostException {
System.out.printf("被[/%s:%s]调用了一次%n", request.getRemoteHost(), request.getRemotePort());
String hostAddress = InetAddress.getLocalHost().getHostAddress() + ":" + port;
return ResultUtils.success(hostAddress);
}
@GetMapping("/get/{id}")
@SentinelResource(value = "getUser")
public CommonResult<User> get(@PathVariable("id") Long id) {
return ResultUtils.success(userService.get(id));
}
public CommonResult<String> handleTest(HttpServletRequest request, BlockException blockException) {
log.error("调用/user/test失败");
return ResultUtils.fail("Sentinel流控,调用失败");
}
}
通过@SentinelResource的blockhandler属性指定对应的异常处理逻辑
注意:
- blockHandler 函数访问范围需要是 public
- 返回类型需要与原方法相匹配
- 参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException
- blockHandler 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 blockHandler 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析
当抛出异常时,若注解指定了异常的处理逻辑那么就直接使用指定的逻辑。若未指定,使用全局的异常处理,否则就返回默认的异常页面
全局异常处理
创建一个全局处理的类,然后实现BlockExceptionHandler
package cuit.epoch.pymjl.handler;
import cn.hutool.json.JSONUtil;
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 cuit.epoch.pymjl.result.CommonResult;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author Pymjl
* @version 1.0
* @date 2022/9/4 19:57
**/
@Component
@Log4j2
public class MyBlockExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e)
throws Exception {
//getRule返回资源、规则的详细信息
log.error("BlockExceptionHandler BlockException================" + e.getRule());
CommonResult<String> result = new CommonResult<>();
result.setSucceed(false);
if (e instanceof FlowException) {
result.setMessage("接口被限流了");
} else if (e instanceof DegradeException) {
result.setMessage("服务降级了");
} else if (e instanceof ParamFlowException) {
result.setMessage("热点参数被限流了");
} else if (e instanceof AuthorityException) {
result.setMessage("授权规则不通过");
}
//返回Json数据
httpServletResponse.setStatus(500);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
try (PrintWriter writer = httpServletResponse.getWriter()) {
writer.write(JSONUtil.toJsonPrettyStr(result));
writer.flush();
} catch (IOException ioException) {
log.error("异常:{}", ioException.toString());
}
}
}
测试
我们启动服务,然后将对应的资源初始化再Sentinel的控制台,然后添加对应的流控规则
访问接口,对应的接口,观察结果
我们可以看到,因为test这个资源指定了对应的异常处理逻辑,所以返回的是handleTest中的处理逻辑
而因为/user/get/1
并未指定,所以就走的全局异常处理逻辑
遇见的bug
注:我这里遇见几个问题,因为水品有限,暂时还不知道这种情况的原因:
- 当给URL配置流控规则时,blockhandler指定的异常处理逻辑失效,如图:
对应的代码:
/**
* @author Pymjl
* @version 1.0
* @date 2022/8/25 12:48
**/
@RestController
@RequestMapping("/user")
@Log4j2
public class UserController {
@Resource
UserService userService;
@Value("${server.port}")
private String port;
@GetMapping("/test")
@SentinelResource(value = "test", blockHandler = "handleTest")
public CommonResult<String> test(HttpServletRequest request) throws UnknownHostException {
System.out.printf("被[/%s:%s]调用了一次%n", request.getRemoteHost(), request.getRemotePort());
String hostAddress = InetAddress.getLocalHost().getHostAddress() + ":" + port;
return ResultUtils.success(hostAddress);
}
public CommonResult<String> handleTest(HttpServletRequest request, BlockException blockException) {
log.error("调用/user/test失败");
return ResultUtils.fail("Sentinel流控,调用失败");
}
}
并没有走指定的异常处理逻辑,而是进入了全局的限流异常处理。当给对应的资源test
添加对应的流控规则时解决该问题
- 当给未指定限流处理逻辑的资源添加对应的流控规则时,抛出的时fallback异常
对应的代码
@GetMapping("/get/{id}")
@SentinelResource(value = "getUser")
public CommonResult<User> get(@PathVariable("id") Long id) {
return ResultUtils.success(userService.get(id));
}
返回的却是fallback异常,按照我的理解应该是走限流的全局异常处理才是
如果有同学知道原因还请指正
get(@PathVariable(“id”) Long id) {
return ResultUtils.success(userService.get(id));
}
[外链图片转存中...(img-EDtQfWvU-1662298885385)]
返回的却是fallback异常,按照我的理解应该是走限流的全局异常处理才是
> 如果有同学知道原因还请指正
项目源码:[gitee](https://gitee.com/pymjl_0/cloud-learn) [github](https://github.com/Pymjl/cloud-learn)
更多推荐
所有评论(0)