xkcoding/spring-boot-demo · warning · RuntimeException

手速太快了,慢点儿吧~

Error message

手速太快了,慢点儿吧~

What it means

Thrown by the Redis-based RateLimiterAspect when a method annotated with @RateLimiter exceeds the maximum request count within the configured time window. The limiter uses a Lua script (limitRedisScript) executed via StringRedisTemplate; when executeTimes returns 0 (window full), shouldLimited returns true and a RuntimeException is thrown. The key combines the method/class name with the client IP, so limits are per-IP.

Source

Thrown at demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/aspect/RateLimiterAspect.java:68

        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. Tune the @RateLimiter(max=N, timeout=T) values to match realistic traffic patterns.
  2. Catch the RuntimeException in a @ControllerAdvice and return HTTP 429 with a Retry-After header.
  3. Address the TODO: incorporate method parameters or a user/session ID into the key to distinguish users behind a shared NAT IP.
  4. Verify the Lua script (limitRedisScript bean) is correctly loaded and Redis connectivity is stable — a script error could cause unexpected limiting behavior.

Example fix

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

// after — custom exception for HTTP 429 mapping
throw new RateLimitException("请求过于频繁,请稍后重试");

// TODO fix for shared-IP key collision:
// before
key = key + SEPARATOR + IpUtil.getIpAddr();
// after — add user or session identifier
key = key + SEPARATOR + IpUtil.getIpAddr() + SEPARATOR + SecurityUtil.getCurrentUsername();
Defensive patterns

Strategy: try-catch

Validate before calling

// No direct pre-check API; the limiter is enforced via AOP + Redis Lua script.
// On the client, implement rate-limit awareness via a Retry-After header from the 429 response.

Try / catch

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

Prevention

When it happens

Trigger: A single IP sends more than `max` requests (default 10) to a @RateLimiter-annotated method within the timeout window (default 1 minute). The Lua script returns 0 on the next call, and the aspect throws.

Common situations: A legitimate user sending rapid requests; a bot or script hitting the endpoint; shared NAT IP causing multiple users behind one address to share a single limit (the TODO comment acknowledges this); rate limit window too short or max too low for real traffic.

Related errors


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