Sentinel的 block 机制是专门处理限流规则,降级规则,热点参数规则。但是当系统内部出现异常,比如:NullPointerException,IIlegalArgumentException等,block就不能很好的处理
于是我们可以采用Sentinel服务熔断 fallback 机制来有效处理系统内部异常,或者说系统业务功能异常
Sentinel服务熔断实战案例
第一:案例需求准备
- 服务方(9001,9002)提供 5条模拟数据
- 消费方(8001)接口需要通过 ID 参数获取服务提供的数据
- 消费方(8001)处理两种异常NullPointerException、IIlegalArgumentException,并对异常进行抛出
- 抛出的异常,需要通过Sentinel进行服务熔断
第二:在服务方(9001,9002)和消费方(8001),分别创建一个实体类
package com.liuyangjava.entity; public class User { private String id; private String name; private Integer age; private String sex; public User() { } public User(String id, String name, Integer age, String sex) { this.id = id; this.name = name; this.age = age; this.sex = sex; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } @Override public String toString() { return "User{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", age=" + age + ", sex='" + sex + '\'' + '}'; } }
第三:在消费方(8001)创建统一返回的ResultData类,此类专门提供数据响应给前端
扩展知识点:
项目中我们会将响应封装成 json 返回,一般我们会将所有接口的数据格式统一, 使前端对数据的操作更一致、轻松。
一般情况下,统一返回数据格式没有固定的格式,只要能描述清楚返回的数据状态以及要返回的具体数据就可以。但是一般会包含 响应状态、 状态码、返回消息、数据 这几部分内容
例如,我们的系统要求返回的基本数据格式如下
列表数据
{ "success": true, "code": 20000, "message":"成功", "data": { "items": [ { "id":"1", "name":"李白", "intro":"李白(701年-762年),字太白,号青莲居士,又号“谪仙人”,唐代伟大的浪漫主义诗人" } ] } }
单条数据:
{ "success": true, "code": 20000, "message":"成功", "data": { "id":"1", "name":"李白", "intro":"李白(701年-762年),字太白,号青莲居士,又号“谪仙人”,唐代伟大的浪漫主义诗人" } }
没有返回数据:
{ "success": true, "code": 20000, "message":"成功", "data": {} }
回调错误数据:
{ "success": false, "code": 20001, "message":"失败", "data": {} }
因此,我们可以定义一个统一的数据返回结果(每个公司对返回结果定义会有些不一样,大家不要对下面的代码太较真)
{ “success": 布尔,//响应是否成功 "code": 数字,//响应码 “message": 字符串,//返回消息 "data": HashMap //返回数据,放在键值对中 }
创建ResultCodeEnum类,记录系统异常的相关信息
package com.liuyangjava.common.enums; public enum ResultCodeEnum { SUCCESS(true, 20000, "成功"), UNKNOWN_REASON(false,20001,"未知错误"), BAD_SQL_GRAMMAR(false, 21001,"sql语法错误"), JSON_PARSE_ERROR(false,21002,"json解析异常"), PARAM_ERROR(false,21003,"参数不正确"); private Boolean success; private Integer code; private String message; ResultCodeEnum(Boolean success, Integer code, String message) { this.success = success; this.code = code; this.message = message; } public Boolean getSuccess() { return success; } public void setSuccess(Boolean success) { this.success = success; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } @Override public String toString() { return "ResultCodeEnum{" + "success=" + success + ", code=" + code + ", message='" + message + '\'' + '}'; } }
创建ResultData类,返回统一定义数据结果
package com.liuyangjava.common.result; import com.liuyangjava.common.enums.ResultCodeEnum; public class ResultData<T> { private Boolean success; private Integer code; private String message; private T data; private ResultData() {} /** * 返回成功 * @return */ public static ResultData ok() { ResultData resultData = new ResultData(); resultData.setSuccess(ResultCodeEnum.SUCCESS.getSuccess()); resultData.setCode(ResultCodeEnum.SUCCESS.getCode()); resultData.setMessage(ResultCodeEnum.SUCCESS.getMessage()); return resultData; } /** * 返回失败 * @return */ public static ResultData error() { ResultData resultData = new ResultData(); resultData.setSuccess(ResultCodeEnum.UNKNOWN_REASON.getSuccess()); resultData.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode()); resultData.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage()); return resultData; } /** * 重新自定义的返回数据信息 * @param resultCodeEnum * @return */ public static ResultData setResultData(ResultCodeEnum resultCodeEnum) { ResultData resultData = new ResultData(); resultData.setSuccess(resultCodeEnum.getSuccess()); resultData.setCode(resultCodeEnum.getCode()); resultData.setMessage(resultCodeEnum.getMessage()); return resultData; } public Boolean getSuccess() { return success; } public void setSuccess(Boolean success) { this.success = success; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getData() { return data; } public void setData(T data) { this.data = data; } }
第四:在服务方(9001,9002)创建NacosUserProviderController接口,且在此接口中提供5条User的模拟数据
package com.liuyangjava.controller; import com.liuyangjava.entity.User; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @RestController public class NacosUserProviderController { private Map<String, User> map = new HashMap<>(); { map.put("1", new User("1", "李白", 28, "男")); map.put("2", new User("2", "杜甫", 25, "男")); map.put("3", new User("3", "苏轼", 23, "男")); map.put("5", new User("5", "辛弃疾", 22, "男")); map.put("6", new User("6", "范仲淹", 21, "男")); } @GetMapping("/nacos/user/provider/{id}") public User getUserInfo(@PathVariable String id) { return map.get(id); } }
第五:在消费方(8001)上创建NacosUserConsumerController接口,通过Feign去调用服务方(9001, 9002)的数据
package com.liuyangjava.controller; import com.liuyangjava.common.result.ResultData; import com.liuyangjava.entity.User; import com.liuyangjava.service.NacosConsumerService; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class NacosUserConsumerController { @Value("${service-url.nacos-provider}") private String serverUrl; @Resource private NacosConsumerService nacosConsumerService; @GetMapping("/nacos/user/consumer/{id}") public ResultData<User> getUserInfo(@PathVariable String id) { User userInfo = nacosConsumerService.getUserInfo(id); if(userInfo == null) { throw new NullPointerException(); } ResultData resultData = ResultData.ok(); resultData.setData(userInfo); return resultData; } }
package com.liuyangjava.service; import com.liuyangjava.entity.User; import com.liuyangjava.service.fallback.NacosConsumerServiceFallBack; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(value = "nacos-provider", fallback = NacosConsumerServiceFallBack.class) public interface NacosConsumerService { @GetMapping("/nacos/provider") String getInfoWithFeign(); // 通过 feign 远程调用9001 或 9002 提供的数据 @GetMapping("/nacos/user/provider/{id}") User getUserInfo(@PathVariable String id); }
第六步:我们加入Sentinel的依赖,并对消费方(8001)的配置文件 application.yml 加入 Sentinel 的配置
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <!--springcloud ailibaba sentinel-datasource-nacos 后续做持久化用到--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
server: port: 8001 spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8858 sentinel: transport: port: 8719 dashboard: 127.0.0.1:8080 application: name: nacos-consumer management: endpoints: web: exposure: include: '*' service-url: nacos-provider: http://nacos-provider
第七步:修改消费方(8001)的接口,加入 @SentinelResource 注解,完成系统业务功能出现异常后的处理方法
package com.liuyangjava.controller; import com.alibaba.csp.sentinel.annotation.SentinelResource; import com.liuyangjava.common.result.ResultData; import com.liuyangjava.entity.User; import com.liuyangjava.service.NacosConsumerService; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class NacosUserConsumerController { @Value("${service-url.nacos-provider}") private String serverUrl; @Resource private NacosConsumerService nacosConsumerService; @GetMapping("/nacos/user/consumer/{id}") @SentinelResource(value = "getUserInfo", fallback = "handlerFallback") public ResultData<User> getUserInfo(@PathVariable String id) throws IllegalAccessException { if("10".equals(id)) { throw new IllegalAccessException("查询数据有误,参数异常..."); } User userInfo = nacosConsumerService.getUserInfo(id); if(userInfo == null) { throw new NullPointerException("查询数据有误, 空指针异常..."); } ResultData resultData = ResultData.ok(); resultData.setData(userInfo); return resultData; } public ResultData handlerFallback(@PathVariable String id, Throwable e) { ResultData resultData = ResultData.error(); resultData.setMessage(e.getMessage()); return resultData; } }
通过测试,当出现空指针异常的时候,handlerFallback 方法就会对异常进行处理
"10".equals(id)