往期回顾

Nacos的安装与配置

Spring Cloud集成Nacos作为注册中心

LoadBalacer集成Nacos实现负载均衡

常见的负载均衡策略分析

Spring Cloud集成Dubbo实现RPC调用

SpringCloud集成Nacos作为配置中心

Nacos整合OpenFegin实现RPC调用

Nacos整合Gateway入门实例

Spring Cloud Gateway的过滤器配置

Nacos整合Gateway实现动态路由

Sentinel的安装与配置

前面我们已经简单的介绍了如何安装与使用Sentinel,接下来我们继续来看看Sentinel的一些核心概念以及特性吧

基本概念回顾

我们前面已经介绍过Sentinel的两个核心概念,这里我们再回顾一下吧:

  • Sentinel 的基本概念有两个,它们分别是:资源和规则
基本概念描述
资源资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如由应用程序提供的服务或者是服务里的方法,甚至可以是一段代码。 我们可以通过 Sentinel 提供的 API 来定义一个资源,使其能够被 Sentinel 保护起来。通常情况下,我们可以使用方法名、URL 甚至是服务名来作为资源名来描述某个资源。
规则围绕资源而设定的规则。Sentinel 支持流量控制、熔断降级、系统保护、来源访问控制和热点参数等多种规则,所有这些规则都可以动态实时调整。

定义资源

Sentinel需要先把可能需要保护的资源定义好,之后再配置规则。也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。

Sentinel提供多种定义资源的方式,分别是:

  • 主流框架的默认适配
  • 抛出异常的方式定义资源
  • 返回布尔值方式定义资源
  • 注解方式定义资源
  • 异步调用支持

这里我不再一一的详细介绍,想要详细了解每种方式的同学可以自行查阅官网

@SentinelResource 注解

Sentinel 支持通过 @SentinelResource 注解定义资源并配置 blockHandlerfallback 函数来进行限流之后的处理。@SentinelResource 注解是 Sentinel 提供的最重要的注解之一,它还包含了多个属性,如下表:

属性说明必填与否使用要求
value用于指定资源的名称必填-
entryTypeentry 类型可选项(默认为 EntryType.OUT)-
blockHandler服务限流后会抛出 BlockException 异常,而 blockHandler 则是用来指定一个函数来处理 BlockException 异常的。 简单点说,该属性用于指定服务限流后的后续处理逻辑。可选项
  1. blockHandler 函数访问范围需要是 public
  2. 返回类型需要与原方法相匹配;
  3. 参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException;
  4. blockHandler 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 blockHandler 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
blockHandlerClass若 blockHandler 函数与原方法不在同一个类中,则需要使用该属性指定 blockHandler 函数所在的类。可选项
  1. 不能单独使用,必须与 blockHandler 属性配合使用;
  2. 该属性指定的类中的 blockHandler 函数必须为 static 函数,否则无法解析。
fallback用于在抛出异常(包括 BlockException)时,提供 fallback 处理逻辑。 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。可选项
  1. 返回值类型必须与原函数返回值类型一致
  2. 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常
  3. fallback 函数默认需要和原方法在同一个类中,若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
fallbackClass若 fallback 函数与原方法不在同一个类中,则需要使用该属性指定 blockHandler 函数所在的类。可选项
  1. 不能单独使用,必须与 fallback 或 defaultFallback 属性配合使用
  2. 该属性指定的类中的 fallback 函数必须为 static 函数,否则无法解析。
defaultFallback默认的 fallback 函数名称,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。 默认 fallback 函数可以针对所以类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。可选项
  1. 返回值类型必须与原函数返回值类型一致
  2. 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常;
  3. defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
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 处理逻辑。若未配置 blockHandlerfallbackdefaultFallback,则被限流降级时会将 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的控制台,然后添加对应的流控规则

image-20220904205022901

访问接口,对应的接口,观察结果

image-20220904203837265

我们可以看到,因为test这个资源指定了对应的异常处理逻辑,所以返回的是handleTest中的处理逻辑

image-20220904204510007

而因为/user/get/1 并未指定,所以就走的全局异常处理逻辑

遇见的bug

注:我这里遇见几个问题,因为水品有限,暂时还不知道这种情况的原因:

  1. 当给URL配置流控规则时,blockhandler指定的异常处理逻辑失效,如图:

image-20220904205237494

对应的代码:

/**
 * @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流控,调用失败");
    }

}

image-20220904205351328

并没有走指定的异常处理逻辑,而是进入了全局的限流异常处理。当给对应的资源test 添加对应的流控规则时解决该问题

  1. 当给未指定限流处理逻辑的资源添加对应的流控规则时,抛出的时fallback异常

image-20220904205645399

对应的代码

    @GetMapping("/get/{id}")
    @SentinelResource(value = "getUser")
    public CommonResult<User> get(@PathVariable("id") Long id) {
        return ResultUtils.success(userService.get(id));
    }

image-20220904205743592

返回的却是fallback异常,按照我的理解应该是走限流的全局异常处理才是

如果有同学知道原因还请指正

项目源码:gitee github

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)



Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐