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
- Raise @RateLimiter max or widen the timeout window
- Return HTTP 429 with Retry-After and have the client back off
- Add method parameters to the limit key for finer granularity
- 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
- Honor 429/Retry-After on the client with exponential backoff
- Tune @RateLimiter max/timeout to real traffic, not the default
- Add method params to the key to avoid the LAN shared-IP collision
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.