发布于2026-07-14 阅读(0)
扫一扫,手机访问
在 Spring 应用开发中,经常会遇到这样一个场景:业务需要响应不定频次的事件(比如用户操作、消息到达),然后触发一次外部 API 调用,而且要求非常严格——每 15 秒最多只能执行一次。听起来很常规,但真正落地时却容易踩坑。如果直接用默认配置的 Token Bucket 算法(比如 RateLimiter.create(1.0 / 15)),它本质上是平滑平均速率,而不是硬性间隔控制——换句话说,它允许“突发”,但你的需求恰恰是“不允许任何突发”,要的是每两次调用之间至少间隔 15 秒,一秒都不能少。
其实完全没必要自己从头实现一个 Leaky Bucket 算法。关键是搞懂一个思路:利用现有限流器的阻塞能力(acquire()),将它当作同步门控,再配合单线程调度,就能等效实现“固定间隔漏出”的效果。下面直接上方案和代码,都是生产环境验证过的。
@Component
public class ApiRateLimitedCaller {
// 每 15 秒发放 1 个令牌 → 平均速率 1/15 QPS,acquire() 将自动阻塞至令牌可用
private final RateLimiter rateLimiter = RateLimiter.create(1.0 / 15.0);
// 使用单线程执行器确保调用串行化,避免并发竞争导致“瞬时双调用”
private final ExecutorService executor = Executors.newSingleThreadExecutor(
r -> new Thread(r, "api-rate-limited-caller-thread")
);
public void triggerApiCall(String payload) {
executor.submit(() -> {
try {
// 阻塞等待获取令牌(实际等待至距上次调用 ≥15s)
rateLimiter.acquire();
// 此处发起真实 HTTP 调用(如 RestTemplate / WebClient)
callExternalApi(payload);
} catch (Exception e) {
// 记录错误,避免异常中断线程池
log.error("Failed to call external API", e);
}
});
}
private void callExternalApi(String payload) {
// 示例:使用 WebClient 异步调用(注意:此处为同步阻塞调用,符合单线程上下文)
// webClient.post().uri("https://api.example.com").bodyValue(payload).retrieve().toBodilessEntity().block();
System.out.println("API called at " + Instant.now());
}
}
System.nanoTime() 保障,完全够用。@PreDestroy 来实现:@PreDestroy
public void shutdown() {
executor.shutdown();
try {
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
Gua va RateLimiter 和 Bucket4j 虽然都是 Token Bucket 的实现,但它们的 acquire() 方法提供了确定性阻塞语义,再配合单线程执行模型,就能完美复现 Leaky Bucket 的“恒定间隔输出”效果。相比自己写定时器或者用 ScheduledTask 来硬凑,这个方案更轻量、更精准、也更容易测试和维护。对于“固定间隔 + 事件触发”这类典型场景,它算得上简洁、可靠、可直接上生产的首选实践。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8