{"record":{"id":"d261436f3ae36356","repo":"xkcoding/spring-boot-demo","slug":"error-d26143","errorCode":null,"errorMessage":"手速太快了，慢点儿吧~","messagePattern":"手速太快了，慢点儿吧~","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"warning","filePath":"demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/aspect/RateLimiterAspect.java","lineNumber":68,"sourceCode":"        Method method = signature.getMethod();\n        // 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解\n        RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class);\n        if (rateLimiter != null) {\n            String key = rateLimiter.key();\n            // 默认用类名+方法名做限流的 key 前缀\n            if (StrUtil.isBlank(key)) {\n                key = method.getDeclaringClass().getName() + StrUtil.DOT + method.getName();\n            }\n            // 最终限流的 key 为 前缀 + IP地址\n            // TODO: 此时需要考虑局域网多用户访问的情况，因此 key 后续需要加上方法参数更加合理\n            key = key + SEPARATOR + IpUtil.getIpAddr();\n\n            long max = rateLimiter.max();\n            long timeout = rateLimiter.timeout();\n            TimeUnit timeUnit = rateLimiter.timeUnit();\n            boolean limited = shouldLimited(key, max, timeout, timeUnit);\n            if (limited) {\n                throw new RuntimeException(\"手速太快了，慢点儿吧~\");\n            }\n        }\n\n        return point.proceed();\n    }\n\n    private boolean shouldLimited(String key, long max, long timeout, TimeUnit timeUnit) {\n        // 最终的 key 格式为：\n        // limit:自定义key:IP\n        // limit:类名.方法名:IP\n        key = REDIS_LIMIT_KEY_PREFIX + key;\n        // 统一使用单位毫秒\n        long ttl = timeUnit.toMillis(timeout);\n        // 当前时间毫秒数\n        long now = Instant.now().toEpochMilli();\n        long expired = now - ttl;\n        // 注意这里必须转为 String,否则会报错 java.lang.Long cannot be cast to java.lang.String\n        Long executeTimes = stringRedisTemplate.execute(limitRedisScript, Collections.singletonList(key), now + \"\", ttl + \"\", expired + \"\", max + \"\");","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/xkcoding/spring-boot-demo/blob/87a142f9604c1a5365b4d24d22c2c11c26a9d5ab/demo-ratelimit-redis/src/main/java/com/xkcoding/ratelimit/redis/aspect/RateLimiterAspect.java#L50-L86","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Tune the @RateLimiter(max=N, timeout=T) values to match realistic traffic patterns.","Catch the RuntimeException in a @ControllerAdvice and return HTTP 429 with a Retry-After header.","Address the TODO: incorporate method parameters or a user/session ID into the key to distinguish users behind a shared NAT IP.","Verify the Lua script (limitRedisScript bean) is correctly loaded and Redis connectivity is stable — a script error could cause unexpected limiting behavior."],"exampleFix":"// before\nthrow new RuntimeException(\"手速太快了，慢点儿吧~\");\n\n// after — custom exception for HTTP 429 mapping\nthrow new RateLimitException(\"请求过于频繁，请稍后重试\");\n\n// TODO fix for shared-IP key collision:\n// before\nkey = key + SEPARATOR + IpUtil.getIpAddr();\n// after — add user or session identifier\nkey = key + SEPARATOR + IpUtil.getIpAddr() + SEPARATOR + SecurityUtil.getCurrentUsername();","handlingStrategy":"try-catch","validationCode":"// No direct pre-check API; the limiter is enforced via AOP + Redis Lua script.\n// On the client, implement rate-limit awareness via a Retry-After header from the 429 response.","typeGuard":null,"tryCatchPattern":"// In a @ControllerAdvice handler — map to HTTP 429\n@ExceptionHandler(RuntimeException.class)\n@ResponseBody\npublic ResponseEntity<?> handleRateLimit(RuntimeException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"手速太快了\")) {\n        return ResponseEntity.status(429)\n            .header(\"Retry-After\", \"60\")\n            .body(ApiResponse.ofMessage(429, \"Too Many Requests\"));\n    }\n    throw e;\n}","preventionTips":["Tune @RateLimiter(max=N, timeout=T) values to match real traffic.","Incorporate user/session ID into the key to avoid NAT-IP collisions (address the TODO).","Verify the limitRedisScript Lua bean is correctly loaded.","Create a custom RateLimitException for clean HTTP 429 mapping."],"tags":["rate-limiting","redis","lua-script","aop","throttling","sliding-window","aspectj"],"backgroundTag":null,"analyzedSha":"87a142f9604c1a5365b4d24d22c2c11c26a9d5ab","analyzedAt":"2026-08-14T01:16:58.217Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}