Sentinel 与 Semaphore 限流/并发控制

很多人第一次接触 Sentinel时,常见问题基本都是这几个:

  1. Sentinel 能不能做接口 QPS 限流
  2. Sentinel 能不能按单个 IP 做限流
  3. Sentinel 能不能做方法级并发数限制
  4. SentinelSemaphore 该怎么选
  5. @SentinelResource 里的 blockHandlerfallback 分别是什么

回答

1 Sentinel 适合做什么

Sentinel 适合做这些保护能力:

  • 资源级 QPS 限流
  • 热点参数限流
  • 熔断降级
  • 系统保护
  • Dubbo / HTTP / 网关入口流控

2 Sentinel 不适合做什么

如果你的需求是:

同一时刻只允许 1 个线程进入某个方法,第二个线程必须稳定被拒绝

Sentinel 不是最合适的工具。

更直接的方案通常是:

  • Semaphore
  • ReentrantLock
  • 分布式场景用 Redis 锁 / ZooKeeper 锁 / 数据库锁

3 选型

  • 资源 QPS 限流:Sentinel
  • 单 IP QPS 限流:Sentinel 热点参数限流
  • 单机内严格方法并发数限制:Semaphore
  • 分布式互斥:分布式锁

Sentinel 的基本使用

你可以把 Sentinel 理解成:

给“资源”打标,然后基于规则去保护这个资源。

这里的“资源”可以是:

  • 一个 HTTP 接口
  • 一个 Service 方法
  • 一个 Dubbo 调用
  • 一个下游依赖调用

比如你定义一个资源名:

public static final String RESOURCE_NAME = "orderService#createOrder";

然后把它挂到一个方法上:

@SentinelResource(value = RESOURCE_NAME)
public void createOrder() {
    // business logic
}

后面只要给这个资源配置规则,Sentinel 就能对它做保护。

@SentinelResource 的进价使用

最常见写法:

@SentinelResource(
        value = RESOURCE_NAME,
        blockHandler = "handleBlock",
        fallback = "handleFallback"
)
public DemoResult doWork(long sleepMs, boolean throwException) {
    if (throwException) {
        throw new IllegalStateException("demo fallback");
    }
    return DemoResult.success();
}

对应处理方法:

public DemoResult handleBlock(long sleepMs, boolean throwException, BlockException ex) {
    return DemoResult.blocked("blocked by sentinel");
}

public DemoResult handleFallback(long sleepMs, boolean throwException, Throwable ex) {
    return DemoResult.fallback("fallback: " + ex.getMessage());
}

 

blockHandlerfallback 的区别

这是使用 Sentinel 时最容易混淆的一点。

 blockHandler

处理的是:

  • 被 Sentinel 规则拦截

例如:

  • QPS 超限
  • 热点参数超限
  • 线程数超限
  • 熔断触发

本质上是:

业务逻辑还没真正执行,Sentinel 先把请求挡住了

fallback

处理的是:

  • 方法执行过程中的业务异常

例如:

  • 主动 throw
  • 空指针
  • 调下游失败
  • 非法状态

本质上是:

业务已经进入方法体,只是执行过程中出错了

简单区分

  • blockHandler = 被 Sentinel 挡住了
  • fallback = 业务自己跑崩了

一个重要的实践经验

blockHandler 的最后一个参数,推荐统一写:BlockException ex 

而不是收窄到某个具体子类,比如: ParamFlowException ex 

原因很简单:

  •  BlockException  是 Sentinel 阻断异常的统一父类
  • 注解模式下用父类匹配最稳
  • 如果签名写得过窄,可能会出现规则已经触发,但没匹配到 blockHandler,最后掉进 fallback

所以通用建议是:

public DemoResult handleBlock(..., BlockException ex)

Sentinel 做普通 QPS 限流

需求:

某个接口或方法,每秒最多只允许通过 1 次

示例代码

@Slf4j
@Service
public class SentinelQpsDemoService {

    public static final String RESOURCE_NAME = "sentinelQpsDemoService#simulateQpsProtectedWork";

    @SentinelResource(
            value = RESOURCE_NAME,
            blockHandler = "handleBlock",
            fallback = "handleFallback"
    )
    public String simulateQpsProtectedWork(long sleepMs, boolean throwException) {
        if (throwException) {
            throw new IllegalStateException("demo fallback");
        }

        sleepQuietly(sleepMs);
        return "SUCCESS";
    }

    public String handleBlock(long sleepMs, boolean throwException, BlockException ex) {
        return "BLOCKED";
    }

    public String handleFallback(long sleepMs, boolean throwException, Throwable ex) {
        return "FALLBACK";
    }

    private void sleepQuietly(long sleepMs) {
        try {
            Thread.sleep(sleepMs);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(ex);
        }
    }
}

 

规则配置

@Configuration
public class SentinelQpsDemoConfig {

    @PostConstruct
    public void initRules() {
        FlowRule rule = new FlowRule();
        rule.setResource(SentinelQpsDemoService.RESOURCE_NAME);
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule.setCount(1);
        FlowRuleManager.loadRules(Collections.singletonList(rule));
    }
}

 

预期行为

在同一秒内连续调用两次:

  • 第一次:SUCCESS
  • 第二次:BLOCKED

Sentinel 做按 IP 的 QPS 限流

关键思路

Sentinel 并不是天然认识“IP”这个概念。

它做的是:

对某个方法参数做热点参数统计

所以如果你想按 IP 限流,本质上要做两步:

  1. 先从请求里解析出 IP
  2. 把 IP 作为方法参数传给 Sentinel

示例代码

@Slf4j
@Service
public class SentinelIpQpsDemoService {

    public static final String RESOURCE_NAME = "sentinelIpQpsDemoService#simulateIpQpsProtectedWork";

    @SentinelResource(
            value = RESOURCE_NAME,
            blockHandler = "handleBlock",
            fallback = "handleFallback"
    )
    public String simulateIpQpsProtectedWork(String clientIp, long sleepMs, boolean throwException) {
        if (throwException) {
            throw new IllegalStateException("demo fallback");
        }

        sleepQuietly(sleepMs);
        return "SUCCESS: " + clientIp;
    }

    public String handleBlock(String clientIp, long sleepMs, boolean throwException, BlockException ex) {
        return "BLOCKED: " + clientIp;
    }

    public String handleFallback(String clientIp, long sleepMs, boolean throwException, Throwable ex) {
        return "FALLBACK: " + ex.getMessage();
    }

    private void sleepQuietly(long sleepMs) {
        try {
            Thread.sleep(sleepMs);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(ex);
        }
    }
}

 

8.3 热点参数规则

@Configuration
public class SentinelIpQpsDemoConfig {

    @PostConstruct
    public void initRules() {
        ParamFlowRule rule = new ParamFlowRule(SentinelIpQpsDemoService.RESOURCE_NAME)
                .setParamIdx(0)
                .setCount(1);

        ParamFlowRuleManager.loadRules(Collections.singletonList(rule));
    }
}

 

这里的意思是:

  • 资源名:sentinelIpQpsDemoService#simulateIpQpsProtectedWork
  • 0 个参数:clientIp
  • 每个不同的 clientIp 各自独立做 QPS 统计
  • 阈值:1

Controller 里怎么传 IP

一个常见写法:

public String resolveClientIp(HttpServletRequest request) {
    String forwardedFor = request.getHeader("X-Forwarded-For");
    if (forwardedFor != null && !forwardedFor.isBlank()) {
        return forwardedFor.split(",")[0].trim();
    }

    String realIp = request.getHeader("X-Real-IP");
    if (realIp != null && !realIp.isBlank()) {
        return realIp.trim();
    }

    return request.getRemoteAddr();
}

 

然后调用:

String clientIp = resolveClientIp(request);
service.simulateIpQpsProtectedWork(clientIp, 10, false);

预期行为

如果阈值是 1

  • 同一个 IP 连续两次请求:第二次应该 BLOCKED
  • 两个不同 IP 各请求一次:都应该成功

Sentinel 能不能做方法并发数限制

答案是:

  • 能做“线程数保护”
  • 但不建议把它当成“严格互斥锁”

示例规则

FlowRule rule = new FlowRule();
rule.setResource("demoMethod");
rule.setGrade(RuleConstant.FLOW_GRADE_THREAD);
rule.setCount(1);

为什么不建议当成严格互斥工具

很多人会把它理解成:

同一时间只能有 1 个线程进去,第二个一定被挡

但在实际测试里,你会发现这种理解过于理想化。

FLOW_GRADE_THREAD 更适合被理解为:

  • 一种线程数维度的流控保护
  • 而不是高精度互斥语义

如果需求是严格的:

当前 JVM 内,这个方法同一时刻最多执行 1 个

那直接用 Semaphore 更合适。

用 Semaphore 做严格方法并发控制

需求:

一个方法在单机内同一时刻只允许 1 个线程执行

示例代码

@Slf4j
@Service
public class SemaphoreConcurrentDemoService {

    private final Semaphore semaphore = new Semaphore(1);

    public String simulateSemaphoreProtectedWork(long sleepMs, boolean throwException) {
        boolean acquired = semaphore.tryAcquire();
        if (!acquired) {
            return "BLOCKED";
        }

        try {
            if (throwException) {
                throw new IllegalStateException("demo fallback");
            }

            Thread.sleep(sleepMs);
            return "SUCCESS";
        } catch (Throwable ex) {
            return "FALLBACK";
        } finally {
            semaphore.release();
        }
    }
}

 

为什么它更适合做严格限制

Semaphore(1) 的语义非常直接:

  • 只有 1 个许可
  • 第一个线程拿到许可后
  • 第二个线程 tryAcquire() 会立刻失败

这个行为比 Sentinel FLOW_GRADE_THREAD 更接近“方法级互斥”的需求。

Sentinel 和 Semaphore 的对比

维度SentinelSemaphore
主要用途 流控保护 并发互斥
适合做 QPS 限流
适合做热点参数限流
适合做严格方法并发控制 不推荐
支持熔断降级
适合做分布式流控体系
单机方法互斥实现成本 一般 很低

常见错误理解

误区一:Sentinel 天然支持“按 IP 限流”

不准确。

更准确的说法是:

Sentinel 可以通过“热点参数限流”来实现按 IP 限流

前提是你把 IP 作为参数传进去。

误区二:fallback 就是限流后的兜底

不准确。

限流后的兜底应该优先走:

blockHandler

fallback 是业务异常兜底。

误区三:线程数限制就等于严格锁

不准确。

线程数流控和互斥锁是两种不同的语义。

推荐测试方式

测普通 QPS

连续两次调用同一个资源:

String first = service.simulateQpsProtectedWork(10, false);
String second = service.simulateQpsProtectedWork(10, false);

预期:

  • 第一次 SUCCESS
  • 第二次 BLOCKED

测同 IP 限流

String first = service.simulateIpQpsProtectedWork("10.10.10.1", 10, false);
String second = service.simulateIpQpsProtectedWork("10.10.10.1", 10, false);

预期:

  • 第一次 SUCCESS
  • 第二次 BLOCKED

测不同 IP 独立统计

String first = service.simulateIpQpsProtectedWork("10.10.10.1", 10, false);
String second = service.simulateIpQpsProtectedWork("10.10.10.2", 10, false);

预期:

  • 两次都成功

测 Semaphore 严格并发限制

使用两个线程同时调用

ExecutorService executor = Executors.newFixedThreadPool(2);

让第一个线程持有许可一段时间,预期第二个线程稳定返回 BLOCKED

写业务代码时的建议

什么时候选 Sentinel

如果你的目标是:

  • 限 QPS
  • 做服务保护
  • 做热点参数流控
  • 做熔断降级

优先选 Sentinel

什么时候选 Semaphore

如果你的目标是:

  • 单机方法内严格并发数限制
  • 当前时刻只允许 1 个线程执行业务

优先选 Semaphore

blockHandler 建议统一写法

建议:

public ReturnType handleBlock(原方法参数..., BlockException ex)

fallback 建议统一写法

建议:

public ReturnType handleFallback(原方法参数..., Throwable ex)

总结

一句话:

Sentinel 的强项是流控保护,Semaphore 的强项是严格并发互斥;QPS、热点参数、熔断这些问题交给 Sentinel,单机内“同一时刻只能执行 1 个”的问题交给 Semaphore。

可以只记这 4 条:

  1. Sentinel 非常适合做 QPS 限流
  2. 按 IP 限流本质是“热点参数限流”
  3. blockHandler 处理 Sentinel 拦截,fallback 处理业务异常
  4. 严格方法并发数限制优先用 Semaphore
posted @ 2026-06-13 09:14  苡沫  阅读(1)  评论(0)    收藏  举报