必须用 tryAcquire(long timeout, TimeUnit unit),不可用 acquire();超时值应远小于数据库慢查询耗时(如200ms),且需满足小于业务最大等待时间、小于P95响应时间;须用 acquired 标志位+finally 确保 release() 不泄漏。

直接结论:别用 acquire(),必须用 tryAcquire(long timeout, TimeUnit unit)
数据库慢查询接口的瞬时限流,核心诉求是「不阻塞、不拖垮线程池、快速失败」。而 acquire() 会无限期挂起线程,一旦慢查询堆积,Tomcat 或 Netty 线程池迅速耗尽,整个服务不可用。必须改用带超时的 tryAcquire(),且超时值要远小于数据库查询预期耗时(比如设为 200ms,而慢查询阈值是 2s)。
tryAcquire() 超时时间设多少才合理
超时值不是拍脑袋定的,它得同时满足两个条件:
- 比业务允许的最大等待时间短(例如用户端 HTTP 超时是 3s,这里最多设 500ms)
- 比后端数据库慢查询的 P95 响应时间更短(比如慢查询 P95 是 1.8s,那
tryAcquire(200, TimeUnit.MILLISECONDS)才能真正挡住“排队等死”的请求) - 不能设为 0(即
tryAcquire()无参版),否则在高并发瞬间大量请求撞上许可已满,会全部立即拒绝,失去缓冲能力
许可证泄漏的三个高发场景
Semaphore 不会自动回收未释放的许可,泄漏一次,可用并发数就永久少一个。最容易出问题的是:
- 数据库操作抛出
SQLException后没进finally块,release()没执行 - 用了
CompletableFuture.supplyAsync()异步查库,但在回调里才release()—— 这违反了「acquire 和 release 必须同一线程」原则 - HTTP 请求中途被客户端断开(如 Nginx
proxy_read_timeout触发),但你的代码还在等 DB 响应,线程中断后未捕获InterruptedException导致漏掉release()
一个防泄漏的最小可靠模板
以下结构能覆盖绝大多数慢查询限流场景,重点看 tryAcquire 调用位置、超时值、以及 release() 的强制兜底:
public void queryUserOrders(String userId) {
// 先尝试拿许可,超时 200ms,不阻塞
if (!semaphore.tryAcquire(200, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("DB concurrency limit exceeded");
}
boolean acquired = true;
try {
// 执行真正的慢查询(JDBC / MyBatis)
List<Order> orders = orderMapper.findByUserId(userId);
return orders;
} catch (Exception e) {
// 记录告警,但不吞异常
log.warn("DB query failed for user: {}", userId, e);
throw e;
} finally {
// 只要 tryAcquire 成功,就必须 release;哪怕上面抛了异常也要放行
if (acquired) {
semaphore.release();
}
}
}
注意:acquired 标志位不是可选的——因为 tryAcquire 可能因中断返回 false,但此时你并不知道是否已部分获取成功(JDK 实现中不会),所以显式控制更安全。
真正难的不是写对这十几行代码,而是把 release() 放进每个可能出口的 finally,并在异步、重试、熔断等增强逻辑里反复验证配对关系。漏一次,压测时可能看不出,上线后某次流量尖峰就悄悄吃掉一个许可,直到某天发现限流阈值永远达不到设定值。

















