不能直接用acquire(),因其会导致线程无限挂起、耗尽tomcat线程池;必须用带超时的tryacquire(long, timeunit),超时值应显著小于接口总超时,并配合db查询超时使用。

为什么不能直接用 acquire() 拦慢查询接口
直接调用 acquire() 会让线程无限期挂起,一旦数据库响应变慢(比如查了 3 秒还没返回),这个线程就卡在 acquire() 上,持续占用 Tomcat 线程池资源;10 个慢查询就能吃光默认的 200 线程,后续所有请求都排队或 503。这不是限流,是自锁。
tryAcquire(long, TimeUnit) 必须带超时,且超时值要小于接口总超时
对慢查询接口做瞬时流控,核心是「快速拒绝」而非「排队等待」。必须用带参数的 tryAcquire(),且超时时间应显著短于整个 HTTP 请求的超时(比如接口设了 5s 超时,这里最多设 tryAcquire(200, TimeUnit.MILLISECONDS))。
常见错误包括:
- 用无参
tryAcquire()—— 它不阻塞但也不等,瞬间失败率太高,无法缓冲突发流量 - 超时设成 1s 甚至更长 —— 和
acquire()几乎没区别,照样拖垮线程池 - 超时值写死在代码里,没和
@Timeout或 Feign/Hystrix 的熔断配置对齐
许可数设置要匹配后端真实瓶颈,不是拍脑袋定 10 或 100
假设你暴露一个 /api/report/export 接口,背后查的是未加索引的大表,DB 连接池最大 8 个,单次查询平均耗 CPU 300ms、IO 等待 1.2s。此时并发压到 6+ 就开始大量超时 —— 那 Semaphore(6) 是合理起点,而不是照搬别人项目的 Semaphore(20)。
验证方式很简单:用 JMeter 压测,观察 DB 连接数、CPU 使用率、平均 RT 三者的拐点。许可数宁可略低,别贪高。
release() 必须在 finally 块里,且不能放在异步回调中
慢查询常伴随异步处理(比如用 CompletableFuture 包一层),但 release() 一定得在原始线程的 finally 块里执行,否则许可证会泄漏。下面这种写法是错的:
if (sem.tryAcquire(200, TimeUnit.MILLISECONDS)) {
CompletableFuture.supplyAsync(() -> doSlowQuery())
.whenComplete((r, e) -> sem.release()); // ❌ release 在子线程,原始线程已退出
}
正确姿势是:
boolean acquired = false;
try {
acquired = sem.tryAcquire(200, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new RuntimeException("Too many concurrent exports");
}
return doSlowQuery();
} finally {
if (acquired) {
sem.release(); // ✅ 保证归还
}
}
注意:如果 doSlowQuery() 内部又用了别的 Semaphore(比如连接池自己的许可),两层 release 不要混淆;每个 acquire() 对应自己作用域内的 release()。
最易被忽略的一点:Semaphore 只控「此刻有多少人在查」,不控「查了多久」。它没法防止某次查询跑满 5 秒还占着许可——所以它必须和 DB 层的 query timeout(如 JDBC 的 socketTimeout)配合使用,否则单个长尾请求就能让整组许可长时间不可用。











