1. 初识@SentinelResource注解
第一次接触Sentinel时,我就被这个神奇的注解吸引了。记得当时项目刚上线就遇到了流量暴增的问题,系统直接被压垮。后来引入Sentinel后,用@SentinelResource注解轻松解决了流量控制问题。这个注解就像是给方法套上了一层防护罩,既能防流量过载,又能处理异常情况。
@SentinelResource最核心的作用就是定义资源。在Sentinel的世界里,任何需要保护的东西都叫资源,比如一个API接口、一个方法调用。通过这个注解,我们可以给方法打上标记,告诉Sentinel:"这个方法很重要,需要你的保护!"
举个例子,我们有个查询用户信息的接口:
@GetMapping("/user/{id}") @SentinelResource(value = "getUserById") public User getUserById(@PathVariable Long id) { return userService.findById(id); }这里的value属性就是资源名称,相当于给这个方法起了个在Sentinel中的别名。为什么要用别名?因为直接使用方法名或URL作为资源名可能会有变化,而value可以保持稳定。
2. 详解注解核心属性
2.1 blockHandler:流量控制的最后防线
blockHandler是我用得最多的属性。它相当于给方法设置了一个"流量超标处理员"。当请求量超过阈值时,Sentinel就会调用这个处理员来应对,而不是直接抛出难看的错误页面。
我遇到过这样一个场景:电商促销时,商品详情接口QPS突然飙升。当时配置是这样的:
@GetMapping("/product/{id}") @SentinelResource(value = "getProductDetail", blockHandler = "handleProductBlock") public Product getProductDetail(@PathVariable Long id) { return productService.getDetail(id); } public Product handleProductBlock(Long id, BlockException ex) { log.warn("商品{}请求被限流", id); return Product.emptyProduct(); // 返回一个空商品对象 }这里有几个关键点需要注意:
- blockHandler方法必须public
- 参数列表要和原方法一致,最后加一个BlockException参数
- 返回值类型要和原方法一致
2.2 fallback:异常处理的优雅方案
fallback就像是方法的"备胎",当方法抛出异常时,就会转向fallback方法。这个特别适合处理那些不太重要的服务,比如获取天气信息的接口:
@GetMapping("/weather") @SentinelResource(value = "getWeather", fallback = "weatherFallback") public Weather getWeather(String city) { return weatherService.query(city); } public Weather weatherFallback(String city, Throwable t) { log.error("获取天气异常", t); return Weather.defaultWeather(); // 返回默认天气数据 }fallback和blockHandler的区别在于:
- blockHandler处理的是Sentinel的BlockException(流量控制异常)
- fallback处理的是业务方法抛出的其他异常
2.3 defaultFallback:全局异常处理
当项目中有很多方法需要相似的fallback逻辑时,defaultFallback就派上用场了。它相当于一个"万能备胎",可以处理多个方法的异常:
@RestController public class UserController { @GetMapping("/user/{id}") @SentinelResource(value = "getUser", defaultFallback = "defaultHandler") public User getUser(@PathVariable Long id) { return userService.findById(id); } @GetMapping("/user/list") @SentinelResource(value = "listUsers", defaultFallback = "defaultHandler") public List<User> listUsers() { return userService.findAll(); } public User defaultHandler(Throwable t) { log.error("服务异常", t); return User.anonymousUser(); } }注意defaultFallback的方法签名比较特殊:
- 可以没有参数
- 或者只有一个Throwable参数
- 返回值类型要兼容所有使用它的方法
3. 高级配置技巧
3.1 使用外部类处理逻辑
随着项目规模扩大,把所有的blockHandler和fallback方法都写在Controller里会让代码变得臃肿。这时可以使用blockHandlerClass和fallbackClass属性,把处理逻辑抽到外部类中。
我通常会创建一个专门的handler包来存放这些类:
public class GlobalBlockHandlers { public static String handleProductBlock(Long id, BlockException ex) { return "系统繁忙,请稍后再试"; } public static String handleOrderBlock(String orderNo, BlockException ex) { return "订单服务暂时不可用"; } } @RestController public class ProductController { @GetMapping("/product/{id}") @SentinelResource(value = "getProduct", blockHandlerClass = GlobalBlockHandlers.class, blockHandler = "handleProductBlock") public String getProduct(@PathVariable Long id) { // 业务逻辑 } }外部类的方法必须是static的,这点要特别注意。我在实际项目中就遇到过因为忘记加static导致的奇怪问题,排查了好久才发现。
3.2 异常过滤exceptionsToIgnore
有些异常我们不想走fallback逻辑,比如参数校验异常,这时就可以用exceptionsToIgnore:
@GetMapping("/order/{id}") @SentinelResource(value = "getOrder", fallback = "orderFallback", exceptionsToIgnore = {IllegalArgumentException.class}) public Order getOrder(@PathVariable String id) { if (!id.matches("\\d+")) { throw new IllegalArgumentException("订单ID格式错误"); } return orderService.getById(id); }这样当参数不合法时,会直接抛出IllegalArgumentException,而不会进入fallback流程。这个配置在处理输入校验时特别有用。
4. 实战中的经验分享
4.1 资源命名的最佳实践
关于value属性的命名,我总结了几条经验:
- 使用业务语义明确的名称,比如"queryUserByPhone"比"getUser"更好
- 遵循统一的命名规范,比如全部小写+下划线分隔
- 避免使用可能变化的名称,比如不要包含版本号
- 对于REST接口,可以结合HTTP方法和路径,如"GET:/api/users"
一个好的命名示例:
@GetMapping("/products/{id}/stock") @SentinelResource(value = "get_product_stock") public int getStock(@PathVariable Long id) { // 查询库存逻辑 }4.2 处理方法的优化建议
在处理方法的实现上,我有几个建议:
- 记录详细的日志,包括参数和异常信息
- 返回有意义的错误信息,方便前端处理
- 考虑性能影响,避免在处理方法中做耗时操作
- 对于不同的异常类型,可以提供不同的处理逻辑
一个较完善的处理方法示例:
public String handleProductBlock(Long id, BlockException ex) { String rule = ex.getRule().getResource(); log.warn("产品{}接口被限流,规则:{}", id, rule); if (ex instanceof FlowException) { return "当前访问人数过多,请稍后再试"; } else if (ex instanceof DegradeException) { return "服务暂时不可用,请稍后重试"; } return "系统繁忙"; }4.3 与Spring Cloud的集成技巧
在Spring Cloud项目中,@SentinelResource可以和Feign、RestTemplate等组件配合使用。比如在Feign客户端上的应用:
@FeignClient(name = "user-service") public interface UserService { @GetMapping("/users/{id}") @SentinelResource(fallback = "getUserFallback") User getUser(@PathVariable Long id); default User getUserFallback(Long id, Throwable t) { return User.anonymousUser(); } }这种用法可以给远程调用加上熔断保护,避免雪崩效应。我在一个分布式项目中就用这种方式成功防止了级联故障。
5. 常见问题排查
5.1 注解不生效的排查步骤
经常有同事问我为什么@SentinelResource没效果,我一般会让他们按以下步骤检查:
- 确认Sentinel依赖和Spring Cloud版本兼容
- 检查是否开启了@EnableSentinel注解
- 确认方法是被Spring管理的Bean调用的
- 检查AOP代理是否生效(CGLIB vs JDK动态代理)
- 查看Sentinel控制台是否有对应的资源显示
最常见的问题是内部方法调用导致AOP失效,比如:
public class OrderService { public void createOrder(Order order) { // 这个方法调用不会触发Sentinel validateOrder(order); } @SentinelResource(value = "validateOrder") public void validateOrder(Order order) { // 校验逻辑 } }5.2 性能优化建议
在高并发场景下,Sentinel的资源统计会带来一定的性能开销。根据我的实测经验,以下几点可以优化性能:
- 合理设置统计间隔(statIntervalInMs)
- 对于不重要资源可以关闭统计
- 使用异步处理减少对主流程的影响
- 适当调整采样率(sampleCount)
一个性能优化的配置示例:
@SentinelResource(value = "highQpsResource", blockHandler = "blockHandler", fallback = "fallback", stats = false) // 关闭统计 public String highQpsMethod() { // 高频调用的方法 }6. 实际项目中的应用案例
6.1 电商系统中的应用
在一个电商项目中,我们是这样使用@SentinelResource的:
商品详情接口:
@GetMapping("/products/{id}") @SentinelResource(value = "product_detail", blockHandlerClass = ProductHandlers.class, blockHandler = "detailBlockHandler", fallback = "detailFallback") public ProductDetail getDetail(@PathVariable Long id) { // 复杂的查询逻辑 }秒杀接口:
@PostMapping("/seckill/{id}") @SentinelResource(value = "seckill_submit", blockHandler = "seckillBlockHandler") public Result submitSeckill(@PathVariable Long id) { // 秒杀业务逻辑 } public Result seckillBlockHandler(Long id, BlockException ex) { return Result.fail("秒杀活动太火爆,请稍后再试"); }6.2 微服务间的调用保护
在微服务架构中,我们使用@SentinelResource保护服务提供方:
@RestController @RequestMapping("/account") public class AccountController { @GetMapping("/{userId}") @SentinelResource(value = "get_account_info", blockHandlerClass = GlobalHandlers.class, blockHandler = "accountBlockHandler") public Account getAccount(@PathVariable String userId) { // 账户查询逻辑 } @PostMapping("/freeze") @SentinelResource(value = "freeze_account", fallback = "freezeFallback") public Result freezeAccount(@RequestBody FreezeRequest request) { // 冻结账户逻辑 } }同时,在服务消费方也进行保护:
@Service public class OrderService { @SentinelResource(value = "create_order", blockHandler = "orderBlockHandler") public Order createOrder(OrderRequest request) { // 调用账户服务 accountClient.freeze(request.getUserId(), request.getAmount()); // 创建订单逻辑 } }这种双重保护机制可以有效防止雪崩效应,确保系统稳定性。