xkcoding/spring-boot-demo · warning · RuntimeException

手速太快了,慢点儿吧~

Error message

手速太快了,慢点儿吧~

What it means

Thrown by the Guava RateLimiterAspect when a method annotated with @RateLimiter(qps=N) receives requests faster than the configured QPS allows. The aspect uses a per-method-name Guava RateLimiter token-bucket; if tryAcquire(timeout, timeUnit) returns false within the wait window, a RuntimeException is thrown. The cache key is method.getName() only, so overloaded methods with the same name share a single limiter.

Source

Thrown at demo-ratelimit-guava/src/main/java/com/xkcoding/ratelimit/guava/aspect/RateLimiterAspect.java:52

    }

    @Around("rateLimit()")
    public Object pointcut(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        // 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解
        RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class);
        if (rateLimiter != null && rateLimiter.qps() > RateLimiter.NOT_LIMITED) {
            double qps = rateLimiter.qps();
            if (RATE_LIMITER_CACHE.get(method.getName()) == null) {
                // 初始化 QPS
                RATE_LIMITER_CACHE.put(method.getName(), com.google.common.util.concurrent.RateLimiter.create(qps));
            }

            log.debug("【{}】的QPS设置为: {}", method.getName(), RATE_LIMITER_CACHE.get(method.getName()).getRate());
            // 尝试获取令牌
            if (RATE_LIMITER_CACHE.get(method.getName()) != null && !RATE_LIMITER_CACHE.get(method.getName()).tryAcquire(rateLimiter.timeout(), rateLimiter.timeUnit())) {
                throw new RuntimeException("手速太快了,慢点儿吧~");
            }
        }
        return point.proceed();
    }
}

View on GitHub (pinned to 87a142f960)

Solutions

  1. Increase the qps value on the @RateLimiter annotation to match expected throughput.
  2. Set a non-zero timeout to allow short bursts to wait for a token rather than failing immediately.
  3. Catch the RuntimeException in a global @ControllerAdvice and return a 429 Too Many Requests response.
  4. Use the method signature (not just name) as the cache key to avoid collisions between overloaded methods.

Example fix

// before — RuntimeException, no HTTP semantics
throw new RuntimeException("手速太快了,慢点儿吧~");

// after — throw a custom exception the handler maps to 429
throw new RateLimitException("请求过于频繁,请稍后重试");

// And in cache key fix:
// before
RATE_LIMITER_CACHE.put(method.getName(), ...)
// after
RATE_LIMITER_CACHE.put(method.getDeclaringClass().getName() + "#" + method.getName(), ...)
Defensive patterns

Strategy: try-catch

Validate before calling

// Before calling a rate-limited method, consider client-side throttling
// No server-side pre-check exists; the limiter is enforced via AOP.
// On the client, implement exponential backoff or a request queue.

Try / catch

// In a @ControllerAdvice handler — map RuntimeException from the aspect to HTTP 429
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public ResponseEntity<?> handleRateLimit(RuntimeException e) {
    if (e.getMessage() != null && e.getMessage().contains("手速太快了")) {
        return ResponseEntity.status(429).body(ApiResponse.ofMessage(429, "Too Many Requests"));
    }
    throw e;
}

Prevention

When it happens

Trigger: A client sends requests to a @RateLimiter-annotated method at a rate exceeding the configured qps value. After the token bucket depletes and the timeout (default 0ms) elapses, tryAcquire returns false and the exception fires.

Common situations: Burst traffic to a rate-limited API endpoint; load testing without accounting for QPS limits; QPS set too low for production traffic; two overloaded methods sharing a name competing for the same bucket.

Related errors


AI-assisted analysis of xkcoding/spring-boot-demo@87a142f960 (2026-08-14). Data as JSON: /api/errors/1250b9f62db1126b. Report an issue: GitHub.