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(); // 返回一个空商品对象
}

这里有几个关键点需要注意:

  1. blockHandler方法必须public
  2. 参数列表要和原方法一致,最后加一个BlockException参数
  3. 返回值类型要和原方法一致

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的方法签名比较特殊:

  1. 可以没有参数
  2. 或者只有一个Throwable参数
  3. 返回值类型要兼容所有使用它的方法

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属性的命名,我总结了几条经验:

  1. 使用业务语义明确的名称,比如"queryUserByPhone"比"getUser"更好
  2. 遵循统一的命名规范,比如全部小写+下划线分隔
  3. 避免使用可能变化的名称,比如不要包含版本号
  4. 对于REST接口,可以结合HTTP方法和路径,如"GET:/api/users"

一个好的命名示例:

@GetMapping("/products/{id}/stock")
@SentinelResource(value = "get_product_stock")
public int getStock(@PathVariable Long id) {
    // 查询库存逻辑
}

4.2 处理方法的优化建议

在处理方法的实现上,我有几个建议:

  1. 记录详细的日志,包括参数和异常信息
  2. 返回有意义的错误信息,方便前端处理
  3. 考虑性能影响,避免在处理方法中做耗时操作
  4. 对于不同的异常类型,可以提供不同的处理逻辑

一个较完善的处理方法示例:

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没效果,我一般会让他们按以下步骤检查:

  1. 确认Sentinel依赖和Spring Cloud版本兼容
  2. 检查是否开启了@EnableSentinel注解
  3. 确认方法是被Spring管理的Bean调用的
  4. 检查AOP代理是否生效(CGLIB vs JDK动态代理)
  5. 查看Sentinel控制台是否有对应的资源显示

最常见的问题是内部方法调用导致AOP失效,比如:

public class OrderService {
    public void createOrder(Order order) {
        // 这个方法调用不会触发Sentinel
        validateOrder(order);
    }
    
    @SentinelResource(value = "validateOrder")
    public void validateOrder(Order order) {
        // 校验逻辑
    }
}

5.2 性能优化建议

在高并发场景下,Sentinel的资源统计会带来一定的性能开销。根据我的实测经验,以下几点可以优化性能:

  1. 合理设置统计间隔(statIntervalInMs)
  2. 对于不重要资源可以关闭统计
  3. 使用异步处理减少对主流程的影响
  4. 适当调整采样率(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());
        // 创建订单逻辑
    }
}

这种双重保护机制可以有效防止雪崩效应,确保系统稳定性。

更多推荐