Skip to content

幂等与限流 ​

这篇解决什么 ​

按钮连点、接口被刷时,要挡住重复提交和过高 QPS。读完能在方法上加 @Idempotent / @RateLimiter,会选 Key 解析器,并分清「窗口内只进一次」和「窗口内最多 N 次」。

默认连本机 Redis 127.0.0.1:6379,库号 0。管理端前缀仍是 /admin-api。幂等、限流和锁共用这套连接。

示意图:Controller 加注解;框架用 Redis 占位或计数后再进业务。

互斥临界区、租约、@Lock4j 见 分布式锁,本篇不重复。

组件位置 ​

名称说明仓库路径
Protection Starter幂等、限流、分布式锁ruoyi-office/yudao-framework/yudao-spring-boot-starter-protection/
幂等装配注册切面和三个 Key 解析器.../idempotent/config/YudaoIdempotentConfiguration.java
限流装配注册切面和五个 Key 解析器.../ratelimiter/config/YudaoRateLimiterConfiguration.java
Redis Starter幂等用 StringRedisTemplate,限流用 RedissonClientruoyi-office/yudao-framework/yudao-spring-boot-starter-redis/
单体入口引入 protection Starter,切面随进程生效ruoyi-office/yudao-server/pom.xml
错误码幂等 900,限流 429yudao-common/.../GlobalErrorCodeConstants.java

这两个注解写在 protection 里,不是 optional。yudao-module-system-server、yudao-module-bpm-server 的依赖目前注释掉了。哪个模块要编译注解,哪个模块的 pom 补同一坐标。

幂等、限流没有单独 yaml。窗口、次数、文案都写在注解上。锁的 acquire-timeout / expire 仍看 分布式锁。

怎么选 ​

名称说明仓库路径
@Idempotent窗口内同 Key 只进一次;成功不删 Key.../idempotent/core/annotation/Idempotent.java
@RateLimiter窗口内同 Key 最多 count 次.../ratelimiter/core/annotation/RateLimiter.java
@Lock4j / RLock互斥,出方法就解锁见 分布式锁
业务唯一键单据级可重试,不靠时间窗口例如 FinanceVoucherSourceServiceImpl#submitSource

幂等还是锁

成功后还要挡一段时间:用 @Idempotent。 成功就放行下一次、只要互斥:用 @Lock4j,不要把 deleteKeyWhenException 理解成「成功也删」。 注解窗口挡不住跨天重放,财务来源那种按账套 + 单据 + 版本查重。

登录页先过验证码,再谈接口限流,见 验证码。Redis 连接与 Key 排查见 Redis 与本地缓存。

幂等 ​

IdempotentAspect 在方法前后拦截。先 SETNX,失败抛 ServiceException;成功才进业务。默认超时 1 秒,单位秒。

示意图:占位成功才往下走;正常返回不删 Key。

名称说明仓库路径
timeout / timeUnit占位存活时间,默认 1 秒@Idempotent
message重复时的 msg,默认「重复请求,请稍后重试」同上
keyResolver默认 DefaultIdempotentKeyResolver同上
keyArg给表达式解析器的 Spring EL同上
deleteKeyWhenException默认 true,失败放开窗口同上
idempotent:%sRedis Key 形态,Value 是空串IdempotentRedisDAO
REPEATED_REQUESTScode 900GlobalErrorCodeConstants
java
@Idempotent(timeout = 10, timeUnit = TimeUnit.SECONDS, message = "正在提交,请勿重复点击")
@PostMapping("/create")
public CommonResult<Long> create(@Valid @RequestBody XxxSaveReqVO reqVO) {
    return success(xxxService.create(reqVO));
}

默认解析器对「方法签名 + 入参」做 MD5。同一入参在窗口内,所有调用方共享一把占位。

名称说明仓库路径
DefaultIdempotentKeyResolverMD5(方法名 + 参数),全局一份.../keyresolver/impl/DefaultIdempotentKeyResolver.java
UserIdempotentKeyResolver再拼 userId、userType.../UserIdempotentKeyResolver.java
ExpressionIdempotentKeyResolver只取 keyArg 的 EL 结果.../ExpressionIdempotentKeyResolver.java
java
@Idempotent(timeout = 5, keyResolver = UserIdempotentKeyResolver.class)
public CommonResult<Boolean> submit(Long id) { /* ... */ }

@Idempotent(timeout = 10,
        keyResolver = ExpressionIdempotentKeyResolver.class,
        keyArg = "#reqVO.id")
public CommonResult<Boolean> retry(XxxSaveReqVO reqVO) { /* ... */ }

按用户挡,换 UserIdempotentKeyResolver。按业务字段挡,换表达式。表达式不再拼方法名,两个方法都写 #reqVO.id 且 id 相同,会抢同一把 idempotent:%s。

成功不删,超时会放行

方法跑完 Key 还在,窗口内同参再来仍是 900。这是防重复提交,不是锁。 业务比 timeout 长,Key 先过期,第二次会再进。估不准就加长。 异常且 deleteKeyWhenException = true 时立刻删 Key,失败后可以重试。

前端按 code !== 0 提示即可,不要再包一层业务错误码。

限流 ​

RateLimiterAspect 在方法前拦截。RateLimiterRedisDAO 拿 Redisson RRateLimiter,tryAcquire() 失败抛 429。默认窗口 1 秒、额度 100。

示意图:先计数,再进业务;限流不在结束后删 Key。

名称说明仓库路径
time / timeUnit窗口,默认 1 秒@RateLimiter
count窗口内次数,默认 100同上
message空则用「请求过于频繁,请稍后重试」同上
keyResolver默认 DefaultRateLimiterKeyResolver同上
keyArg给 ExpressionRateLimiterKeyResolver同上
rate_limiter:%sRedis Key;RateType.OVERALLRateLimiterRedisDAO
TOO_MANY_REQUESTScode 429GlobalErrorCodeConstants

自定义表达式要用 ExpressionRateLimiterKeyResolver,不要写成幂等那个类。

java
@RateLimiter(count = 10, timeUnit = TimeUnit.MINUTES)
@PostMapping("/create")
public CommonResult<Long> create(@Valid @RequestBody XxxSaveReqVO reqVO) {
    return success(xxxService.create(reqVO));
}

上面是全局:同一入参,所有人合计每分钟 10 次。

名称说明仓库路径
DefaultRateLimiterKeyResolverMD5(方法名 + 参数),全局一份.../impl/DefaultRateLimiterKeyResolver.java
UserRateLimiterKeyResolver再拼登录用户.../UserRateLimiterKeyResolver.java
ClientIpRateLimiterKeyResolver再拼客户端 IP.../ClientIpRateLimiterKeyResolver.java
ServerNodeRateLimiterKeyResolver再拼 host@pid,按进程.../ServerNodeRateLimiterKeyResolver.java
ExpressionRateLimiterKeyResolver只取 keyArg.../ExpressionRateLimiterKeyResolver.java

登录短信 ​

AuthController#smsLogin 留了按手机号限流,默认注释。打开后:同一手机号 60 秒内最多 6 次。

名称说明仓库路径
POST /system/auth/sms-login短信登录yudao-module-system-server/.../auth/AuthController.java
拟开注解time = 60, count = 6,keyArg = "#reqVO.mobile"同上注释
java
@PostMapping("/sms-login")
@PermitAll
@RateLimiter(time = 60, count = 6,
        keyResolver = ExpressionRateLimiterKeyResolver.class,
        keyArg = "#reqVO.mobile")
public CommonResult<AuthLoginRespVO> smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) {
    return success(authService.smsLogin(reqVO));
}

要编译这段,先在 yudao-module-system-server/pom.xml 恢复 protection 依赖。发验证码本身另有间隔,不是这个注解。

名称说明仓库路径
send-frequency同一手机号间隔 1 分钟yudao-module-system-server/.../application.yaml 的 yudao.sms-code
send-maximum-quantity-per-day每天 10 条同上
校验查上次记录,过快抛 SMS_CODE_SEND_TOO_FASTSmsCodeServiceImpl#createSmsCode

验证码组件自己的 get / check 分钟限流,见 验证码。

窗口单位会被收成秒

RateLimiterRedisDAO 用 timeUnit.toSeconds(time) 配 Redisson。MILLISECONDS 且不足 1000,间隔会变成 0。 新建限流器后会 expire 同一窗口,避免 Key 常驻。改 count / 窗口会 setRate 再设过期。 表达式 Key 同样不带方法名:两个接口都按 #reqVO.mobile 限流,会共用 rate_limiter:%s。

本机连不上 127.0.0.1:6379 时,先看 Redis 进程和 application-local.yaml,不要改成别的环境的地址。

配置与操作 ​

防重复提交加 @Idempotent,限流加 @RateLimiter,锁见分布式锁篇。Key 表达式不要和别的接口撞车。要 Redis。

开启见 框架层。

相关篇 ​

联系我们

获取报价、演示和二开方案

微信咨询二维码

微信咨询

17156169080

添加时备注「RuoYi Office」

在线体验商业版