一、前言

Sentinel提供了 @SentinelResource 注解用于定义资源,并提供可选的异常回退和Block回退。异常回退指的是@SentinelResource注解标注的方法发生Java异常时的回退处理;Block回退指的是当@SentinelResource资源访问不符合Sentinel控制台定义的规则时的回退(默认返回Blocked by Sentinel (flow limiting))。这节简单记录下该注解的用法。

二、框架搭建

2.1 项目搭建

使用IDEA创建一个maven项目,artifactId为spring-cloud-alibaba-sentinelresource,然后在其下面创建两个Module(Spring Boot项目),artifactId分别为consumer和provider,充当服务消费端和服务提供端:

spring-cloud-alibaba-sentinelresource的pom内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wno704</groupId>
<artifactId>spring-cloud-alibaba-sentinelresource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Alibaba-SentinelResource</name>
<description>Demo project for Spring Boot</description>
<packaging>pom</packaging>

<properties>
<java.version>1.8</java.version>
<spring-cloud-alibaba.version>2.2.3.RELEASE</spring-cloud-alibaba.version>
</properties>

<modules>
<module>consumer</module>
<module>provider</module>
</modules>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

引入了spring-boot-starter-web和spring-cloud-starter-alibaba-nacos-discovery服务注册发现依赖。

provider的pom的内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.wno704</groupId>
<artifactId>spring-cloud-alibaba-sentinelresource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>provider</artifactId>
<name>Alibaba-Provider</name>
<description>Demo project for Spring Boot</description>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

consumer的pom内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.wno704</groupId>
<artifactId>spring-cloud-alibaba-sentinelresource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>consumer</artifactId>
<name>Alibaba-Consumer</name>
<description>Demo project for Spring Boot</description>

<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

因为要演示在消费端使用@SentinelResource注解,所以我们引入了spring-cloud-starter-alibaba-sentinel依赖。

2.2 项目配置

provider的配置文件application.yml内容如下:

1
2
3
4
5
6
7
8
server:
port: 8900
spring:
application:
name: provider
cloud:
nacos:
server-addr: 127.0.0.1:8848

配置了端口号,服务名和nacos地址。

consumer的配置文件application.yml内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8901
spring:
application:
name: consumer
cloud:
nacos:
server-addr: 127.0.0.1:8848
sentinel:
transport:
dashboard: 127.0.0.1:8080
port: 8719

配置了端口号,服务名,nacos地址和sentinel控制台地址等。

三、基本用法

3.1 创建controller

我们在provider下添加一个REST资源。在provider的cc.mrbird.provider目录下新建controller包,然后在该包下新建GoodsController:

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("goods")
public class GoodsController {

@GetMapping("buy/{name}/{count}")
public String buy(@PathVariable String name, @PathVariable Integer count) {
return String.format("购买%d份%s", count, name);
}
}

接着在consumer端通过Ribbon消费这个资源。在consumer的com.wno704.alibaba的目录下新建config文件夹,新建ConsumerConfig配置类,注册RestTemplate。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class ConsumerConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}

@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}

}

在consumer的com.wno704.alibaba下新建controller包,然后在该包下新建ConsumeController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RestController
public class ConsumeController {

@Autowired
private LoadBalancerClient loadBalancerClient;

@Autowired
private RestTemplate restTemplate;

@GetMapping("buy/{name}/{count}")
@SentinelResource(value = "buy", fallback = "buyFallback", blockHandler = "buyBlock")
public String buy(@PathVariable String name, @PathVariable Integer count) {
if (count >= 20) {
throw new IllegalArgumentException("购买数量过多");
}
if ("wno704".equalsIgnoreCase(name)) {
throw new NullPointerException("已售罄");
}
Map<String, Object> params = new HashMap<>(2);
params.put("name", name);
params.put("count", count);

ServiceInstance serviceInstance = loadBalancerClient.choose("provider");
String path = String.format("http://%s:%s/goods/buy/{name}/{count}", serviceInstance.getHost(), serviceInstance.getPort());
return this.restTemplate.getForEntity(path, String.class, params).getBody();
}

// 异常回退
public String buyFallback(@PathVariable String name, @PathVariable Integer count, Throwable throwable) {
return String.format("【进入fallback方法】购买%d份%s失败,%s", count, name, throwable.getMessage());
}

// sentinel回退
public String buyBlock(@PathVariable String name, @PathVariable Integer count, BlockException e) {
return String.format("【进入blockHandler方法】购买%d份%s失败,当前购买人数过多,请稍后再试", count, name);
}
}

在buy方法中,我们通过Ribbon的RestTemplate访问provider的/goods/buy接口。当count参数大于20或者name参数的值为miband的时候,方法将抛出异常。buy方法上使用@SentinelResource注解标注,标识为一个sentinel资源,资源名称为buy,并且配置了fallback方法和blockHandler方法。

如前面所说,当buy方法本身抛出异常时,会进入fallback指定的回退方法中;当buy方法调用不符合sentinel控制台规定的规则(如流控规则,降级规则等)时,会进入blockHander指定的block方法中。为了确保成功地进入回退方法(成功反射),它们必须满足以下规则:

  • 函数访问范围需要是public;
  • Fallback函数,函数签名与原函数一致或末尾加一个Throwable类型的参数;
  • Block异常处理函数,参数最后多一个BlockException,其余与原函数一致。

3.2 测试

启动provider、consumer、nacos和sentinel控制台,http访问: http://localhost:8901/buy/ipad/2

我们在sentinel控制台中添加如下流控规则:

QPS阈值为2。

然后快速访问 http://localhost:8901/buy/ipad/2

可以看到,当方法访问不符合sentinel控制台规则时,进入的是blockHandler指定的回退方法。

如果访问: http://localhost:8901/buy/ipad/21

或者: http://localhost:8901/buy/wno704/2

方法自身抛出异常引发回退,进入的是fallback指定的回退方法。

四、其他属性

在当前类中编写回退方法会使得代码变得冗余耦合度高,我们可以将回退方法抽取出来到一个指定类中。

在com.wno704.alibaba包下新建reveal包,然后在该包下新建BuyBlockHandler:

1
2
3
4
5
6
7
8
public class BuyBlockHandler {

// sentinel回退
public static String buyBlock(@PathVariable String name, @PathVariable Integer count, BlockException e) {
return String.format("【进入blockHandler方法】购买%d份%s失败,当前购买人数过多,请稍后再试", count, name);
}

}

可以看到我们只是将buyBlock方法挪到了BuyBlockHandler中,不过这里的方法必须是static的。

接着新建BuyFallBack:

1
2
3
4
5
6
7
8
public class BuyFallBack {

// 异常回退
public static String buyFallback(@PathVariable String name, @PathVariable Integer count, Throwable throwable) {
return String.format("【进入fallback方法】购买%d份%s失败,%s", count, name, throwable.getMessage());
}

}

这样BuyController的代码就可以精简为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@RestController
public class ConsumeController {

@Autowired
private LoadBalancerClient loadBalancerClient;

@Autowired
private RestTemplate restTemplate;

@GetMapping("buy/{name}/{count}")
@SentinelResource(value = "buy",
fallback = "buyFallback",
fallbackClass = BuyFallBack.class,
blockHandler = "buyBlock",
blockHandlerClass = BuyBlockHandler.class
)
public String buy(@PathVariable String name, @PathVariable Integer count) {
if (count >= 20) {
throw new IllegalArgumentException("购买数量过多");
}
if ("wno704".equalsIgnoreCase(name)) {
throw new NullPointerException("已售罄");
}
Map<String, Object> params = new HashMap<>(2);
params.put("name", name);
params.put("count", count);

ServiceInstance serviceInstance = loadBalancerClient.choose("provider");
String path = String.format("http://%s:%s/goods/buy/{name}/{count}", serviceInstance.getHost(), serviceInstance.getPort());
return this.restTemplate.getForEntity(path, String.class, params).getBody();
}

}

此外我们也可以当遇到某个类型的异常时,不进行回退。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RestController
public class ConsumeController {

@Autowired
private LoadBalancerClient loadBalancerClient;

@Autowired
private RestTemplate restTemplate;

@GetMapping("buy/{name}/{count}")
@SentinelResource(value = "buy",
fallback = "buyFallback",
fallbackClass = BuyFallBack.class,
blockHandler = "buyBlock",
blockHandlerClass = BuyBlockHandler.class,
exceptionsToIgnore = NullPointerException.class
)
public String buy3(@PathVariable String name, @PathVariable Integer count) {
if (count >= 20) {
throw new IllegalArgumentException("购买数量过多");
}
if ("wno704".equalsIgnoreCase(name)) {
throw new NullPointerException("已售罄");
}
Map<String, Object> params = new HashMap<>(2);
params.put("name", name);
params.put("count", count);

ServiceInstance serviceInstance = loadBalancerClient.choose("provider");
String path = String.format("http://%s:%s/goods/buy/{name}/{count}", serviceInstance.getHost(), serviceInstance.getPort());
return this.restTemplate.getForEntity(path, String.class, params).getBody();
}

}

exceptionsToIgnore指定,当遇到空指针异常时,不回退。

重启consumer,http访问: http://localhost:8901/buy/wno704/2

可以看到,此次并没有进行回退,而是直接返回error page。