YunaiV/ruoyi-vue-pro · warning · ServiceException
900
900
Error message
重复请求,请稍后重试
What it means
yudao's IdempotentAspect acquires a Redis key per request (keyResolver + idempotentRedisDAO.setIfAbsent). If the key already exists within the timeout window, the request is a duplicate and it throws ServiceException(REPEATED_REQUESTS code 900, default message '重复请求,请稍后重试'). It is intentional behavior, not a bug — the @Idempotent annotation requested it.
Source
Thrown at yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/aop/IdempotentAspect.java:52
public IdempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass);
this.idempotentRedisDAO = idempotentRedisDAO;
}
@Around(value = "@annotation(idempotent)")
public Object aroundPointCut(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
// 获得 IdempotentKeyResolver
IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());
Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");
// 解析 Key
String key = keyResolver.resolver(joinPoint, idempotent);
// 1. 锁定 Key
boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit());
// 锁定失败,抛出异常
if (!success) {
log.info("[aroundPointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs());
throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message());
}
// 2. 执行逻辑
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
// 3. 异常时,删除 Key
// 参考美团 GTIS 思路:https://tech.meituan.com/2016/09/29/distributed-system-mutually-exclusive-idempotence-cerberus-gtis.html
if (idempotent.deleteKeyWhenException()) {
idempotentRedisDAO.delete(key);
}
throw throwable;
}
}
}
View on GitHub (pinned to 0418084e22)
Solutions
- On the client, disable the submit button after first click and only retry after the timeout.
- Tune @Idempotent(timeout, timeUnit, keyResolver) so the window and key match the business semantics.
- If duplicates are expected and harmless, set deleteKeyWhenException=true (already handled) or remove the annotation.
- Use a business-id-based keyResolver so distinct business requests do not collide.
Example fix
// before: too-broad key collides across users @Idempotent(timeout = 10, keyResolver = DefaultIdempotentKeyResolver.class) // after: include business id in the key @Idempotent(timeout = 10, keyResolver = ExprIdempotentKeyResolver.class, key = "#request.orderNo")
Defensive patterns
Strategy: try-catch
Validate before calling
String key = keyResolver.resolver(joinPoint, idempotent); boolean first = redisTemplate.opsForValue().setIfAbsent(key, "1", timeout, unit); if (!first) throw new ServiceException(REPEATED_REQUESTS);
Type guard
null
Try / catch
try { return aspect.aroundPointCut(pjp, idempotent); }
catch (ServiceException e) { if (e.getCode()==900) return ResponseEntity.status(409).body("duplicate"); throw e; } Prevention
- Disable the submit button after first click
- Tune @Idempotent timeout/key to business semantics
- Use a business-id keyResolver to avoid collisions
When it happens
Trigger: A client retried/submitted the same @Idempotent-guarded request twice within the timeout window (same key from DefaultIdempotentKeyResolver = method+args, or from Expr/Args resolvers); double-click submit; automatic retry storm.
Common situations: Front-end double submit; network retry by an HTTP client; an idempotent operation whose key is too coarse (collides across distinct business calls).
Related errors
AI-assisted analysis of YunaiV/ruoyi-vue-pro@0418084e22 (2026-08-14).
Data as JSON: /api/errors/40631fa07fd625f0.
Report an issue: GitHub.