xkcoding/spring-boot-demo · warning · RuntimeException

手速太快了,慢点儿吧~

Error message

手速太快了,慢点儿吧~

What it means

RateLimiterAspect.pointcut runs a Redis Lua script (shouldLimited); when the request count in the window reaches/exceeds max the script returns 0 and the aspect throws RuntimeException('手速太快了,慢点儿吧~'). The runtime source is RateLimiterAspect.java:68; README:179 documents the same logic.

Source

Thrown at demo-ratelimit-redis/README.md:179

        Method method = signature.getMethod();
        // 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解
        RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class);
        if (rateLimiter != null) {
            String key = rateLimiter.key();
            // 默认用类名+方法名做限流的 key 前缀
            if (StrUtil.isBlank(key)) {
                key = method.getDeclaringClass().getName()+StrUtil.DOT+method.getName();
            }
            // 最终限流的 key 为 前缀 + IP地址
            // TODO: 此时需要考虑局域网多用户访问的情况,因此 key 后续需要加上方法参数更加合理
            key = key + SEPARATOR + IpUtil.getIpAddr();

            long max = rateLimiter.max();
            long timeout = rateLimiter.timeout();
            TimeUnit timeUnit = rateLimiter.timeUnit();
            boolean limited = shouldLimited(key, max, timeout, timeUnit);
            if (limited) {
                throw new RuntimeException("手速太快了,慢点儿吧~");
            }
        }

        return point.proceed();
    }

    private boolean shouldLimited(String key, long max, long timeout, TimeUnit timeUnit) {
        // 最终的 key 格式为:
        // limit:自定义key:IP
        // limit:类名.方法名:IP
        key = REDIS_LIMIT_KEY_PREFIX + key;
        // 统一使用单位毫秒
        long ttl = timeUnit.toMillis(timeout);
        // 当前时间毫秒数
        long now = Instant.now().toEpochMilli();
        long expired = now - ttl;
        // 注意这里必须转为 String,否则会报错 java.lang.Long cannot be cast to java.lang.String
        Long executeTimes = stringRedisTemplate.execute(limitRedisScript, Collections.singletonList(key), now + "", ttl + "", expired + "", max + "");

View on GitHub (pinned to 87a142f960)

Solutions

  1. Raise @RateLimiter max or widen the timeout window
  2. Return HTTP 429 with Retry-After and have the client back off
  3. Add method parameters to the limit key for finer granularity
  4. Address the LAN shared-IP key collision noted in the TODO

Example fix

// before
throw new RuntimeException('手速太快了,慢点儿吧~');
// after (in a controller advice)
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> onRateLimit(RuntimeException e) {
    if ('手速太快了,慢点儿吧~'.equals(e.getMessage())) {
        return ResponseEntity.status(429).header('Retry-After', '2').body('rate limited');
    }
    throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

// best-effort pre-check: remaining quota before the guarded call
Long used = stringRedisTemplate.execute(limitRedisScript, Collections.singletonList('limit:' + key), now + '', ttl + '', expired + '', (max - 1) + '');
if (used != null && used == 0) { /* back off instead of calling */ }

Try / catch

long backoff = 500;
for (int i = 0; i < 3; i++) {
    try { return point.proceed(); }
    catch (RuntimeException e) {
        if (!'手速太快了,慢点儿吧~'.equals(e.getMessage())) throw e;
        Thread.sleep(backoff); backoff *= 2;
    }
}

Prevention

When it happens

Trigger: A client exceeds @RateLimiter(max=, timeout=) within the window for the same key, where the key is the custom key or class.method + client IP.

Common situations: Burst or loop traffic; max configured too low; multiple users behind one NAT IP colliding on the key (the code TODO explicitly flags this LAN shared-IP problem); a client with no backoff.

Related errors


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