一、前言
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。