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

  1. On the client, disable the submit button after first click and only retry after the timeout.
  2. Tune @Idempotent(timeout, timeUnit, keyResolver) so the window and key match the business semantics.
  3. If duplicates are expected and harmless, set deleteKeyWhenException=true (already handled) or remove the annotation.
  4. 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

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.