13 KiB
rdms-spring-boot-starter-protection
模块定位
rdms-spring-boot-starter-protection 用于提供一组服务保护能力,减少业务代码里重复编写“防重复提交、限流、分布式锁、接口签名校验”这类横切逻辑。
当前模块实际包含四块能力:
- 幂等:防止同一请求在短时间内被重复执行,例如重复提交、重复点击
- 分布式锁:在并发场景下保证同一业务动作同一时间只有一个线程或节点执行
- 限流:限制单位时间内的访问次数,避免接口被刷爆或高频调用
- HTTP API 签名:校验调用方身份、请求时效和随机数,防止伪造请求和重放攻击
它的目标不是提供复杂的治理平台,而是把常见保护能力做成可声明、可复用、可分布式生效的基础设施。
设计思路
1. 用注解声明保护规则,用 AOP 统一执行
模块里的幂等、限流、签名校验都不是要求业务手写 Redis 判断逻辑,而是通过注解声明规则,再由 AOP 统一拦截执行:
@Idempotent@RateLimiter@ApiSignature
这样业务代码只负责说明“这个方法需要什么保护”,具体的 Redis Key 生成、重复请求判断、限流判断、签名验证由框架层统一处理。
2. 把状态统一放到 Redis,面向分布式部署
这个模块的保护能力不是单机内存级别,而是默认面向多实例部署场景:
- 幂等使用
StringRedisTemplate - API 签名防重放使用
StringRedisTemplate - 限流使用
Redisson的RRateLimiter - 分布式锁接入
lock4j-redisson-spring-boot-starter
这意味着它的核心价值不是“本机防抖”,而是“多个节点共享保护状态”。
3. 单一能力单独建模,避免职责混杂
从实现上看,模块有一个比较明确的边界划分:
- 幂等用于“同一请求短时间内只允许执行一次”
- 分布式锁用于“并发竞争下只允许一个线程/节点进入”
- 限流用于“单位时间窗口内限制请求次数”
- API 签名用于“校验调用方身份、请求时效和防重放”
例如幂等组件没有扩展成“成功后立即删 Key”的锁语义,而是把这类能力交给 Lock4j 处理。
区分这两类能力时,可以抓住一个核心点:
- 幂等控制的是“短时间内不要重复执行同一类请求”
- 分布式锁控制的是“同一时间谁有资格进入临界区执行”
例如:
- 用户连续点击两次“提交订单”,更适合用幂等
- 两个线程同时刷新同一份缓存,只允许一个线程进入执行,更适合用分布式锁
一个常见误区是把长耗时任务也交给幂等处理。假设幂等窗口配置为 3 秒,但某个任务实际执行了 20 秒:
- 第一个请求在第 0 秒进入
- 幂等 Key 在第 3 秒过期
- 第二个请求在第 5 秒再次进入
- 此时第一个任务还没执行完,但第二个请求已经有机会再次执行
所以幂等更偏“防重复请求”,而不是“在整个执行期间控制线程执行权”。如果真正关心的是执行过程中的互斥,应优先使用分布式锁。
4. 用 KeyResolver 抽象不同粒度的保护范围
幂等和限流都没有把 Key 生成规则写死,而是抽成了 KeyResolver:
- 默认级别:方法名 + 参数
- 用户级别:方法名 + 参数 + 当前用户
- IP 级别:方法名 + 参数 + 客户端 IP
- 节点级别:方法名 + 参数 + 当前服务节点
- 表达式级别:通过 SpEL 自定义 Key
这样模块能在“全局、按用户、按 IP、按节点、按业务字段”之间切换,而不是只能支持一种固定粒度。
5. 自动装配按功能块拆分
自动配置入口见:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
当前注册了四个自动配置类:
RdmsIdempotentConfigurationRdmsLock4jConfigurationRdmsRateLimiterConfigurationRdmsApiSignatureAutoConfiguration
这说明模块是按能力块拆分装配的,而不是堆在一个总配置类里。
功能说明
1. 幂等
核心类:
IdempotentIdempotentAspectIdempotentRedisDAO
实现方式:
- AOP 拦截带
@Idempotent的方法 - 通过
IdempotentKeyResolver解析 Redis Key - 调用
SETNX + EXPIRE语义的setIfAbsent - 锁定失败时抛出“重复请求”异常
- 业务异常时可按配置删除 Key,允许后续重试
适合场景:
- 防止用户双击按钮
- 防止表单重复提交
- 防止短时间内重复触发同一个业务动作
典型案例:
- 创建工单时,前端连续点击两次“提交”,只允许创建一次
- 导入任务提交后,用户刷新页面再次点击“开始导入”,短时间内不允许重复发起
- 审批流提交节点时,浏览器因为网络抖动自动重发请求,后端只执行一次
边界说明:
- 这里的幂等更偏“短时间窗口防重复请求”,不适合作为支付、退款这类长期业务幂等的唯一方案
- 对于支付回调、订单状态推进这类场景,更可靠的做法仍然是基于业务唯一号和持久化状态做幂等判断,必要时再配合分布式锁
可选 KeyResolver:
DefaultIdempotentKeyResolverUserIdempotentKeyResolverExpressionIdempotentKeyResolver
示例:
@Idempotent(timeout = 3, message = "请勿重复提交")
public Long createOrder(OrderCreateReqVO reqVO) {
return orderService.createOrder(reqVO);
}
如果需要按某个参数做幂等,可以使用表达式:
@Idempotent(
keyResolver = ExpressionIdempotentKeyResolver.class,
keyArg = "#reqVO.no",
timeout = 10
)
public void submit(OrderReqVO reqVO) {
}
如果接口已经完成登录鉴权,也可以按“当前登录用户 + 接口参数”做幂等:
@Idempotent(
keyResolver = UserIdempotentKeyResolver.class,
timeout = 3,
message = "请勿重复提交"
)
public CommonResult<Long> createOrder(OrderCreateReqVO reqVO) {
return success(orderService.createOrder(reqVO));
}
这种方式不要求把 userId 显式放在请求参数里,而是从当前登录上下文中获取用户信息,适合“同一个用户短时间内不能重复提交同一类请求”的场景。
2. 限流
核心类:
RateLimiterRateLimiterAspectRateLimiterRedisDAO
实现方式:
- AOP 在方法执行前拦截
- 通过
RateLimiterKeyResolver解析限流 Key - 使用 Redisson 的
RRateLimiter设置速率 - 超过限制时抛出“请求过于频繁”异常
适合场景:
- 接口防刷
- 高频查询保护
- 短时间内限制短信、验证码、导出等高成本操作
- 对开放接口或公网接口做基础流量保护
典型案例:
- 短信验证码接口限制“1 分钟内最多发送 1 次”
- 导出 Excel 接口限制“10 分钟内最多导出 3 次”
- 登录接口按 IP 限制访问频率,防止暴力尝试
- 某个开放查询接口按调用方或用户维度限制 QPS,避免被刷爆
可选 KeyResolver:
DefaultRateLimiterKeyResolverUserRateLimiterKeyResolverClientIpRateLimiterKeyResolverServerNodeRateLimiterKeyResolverExpressionRateLimiterKeyResolver
示例:
@RateLimiter(count = 5, time = 1, timeUnit = TimeUnit.MINUTES, message = "请求过于频繁")
public CommonResult<Boolean> sendSmsCode(String mobile) {
return success(true);
}
如果希望按用户限流:
@RateLimiter(
count = 10,
time = 1,
timeUnit = TimeUnit.MINUTES,
keyResolver = UserRateLimiterKeyResolver.class
)
public PageResult<OrderRespVO> pageMyOrders(OrderPageReqVO reqVO) {
return orderService.pageMyOrders(reqVO);
}
3. HTTP API 签名
核心类:
ApiSignatureApiSignatureAspectApiSignatureRedisDAO
实现方式:
- 从请求头中读取
appId、timestamp、nonce、sign - 校验时间戳是否过期
- 校验
nonce是否已使用,防止重放 - 根据
appId从 Redis 中获取appSecret - 按“请求参数 + 请求体 + 请求头 + 密钥”构建签名字符串
- 使用
SHA-256计算服务端签名并比对
适合场景:
- 开放接口
- 系统间调用
- 对请求来源和重放攻击有要求的 HTTP 接口
典型案例:
- 第三方系统调用内部开放 API 时,使用
appId + appSecret做签名校验 - 支付平台、供应商平台、合作方系统调用回调接口时,先校验签名再进入业务处理
- 对外提供的 B2B 接口要求请求在 60 秒内有效,并且
nonce只能使用一次
示例:
@ApiSignature(timeout = 60)
public CommonResult<String> callback() {
return success("ok");
}
当前切面是按 @annotation(signature) 拦截的,因此实操上应优先加在方法上。
4. 分布式锁
核心类:
RdmsLock4jConfigurationDefaultLockFailureStrategy
这部分没有重复实现分布式锁,而是接入 Lock4j,并补了一层默认失败策略:
- 如果类路径存在
Lock4j,则启用配置 - 在获取锁失败时,统一抛出项目内的
ServiceException - 保持锁失败时的错误处理风格一致
适合场景:
- 防止同一业务动作并发执行
- 串行化关键资源操作
- 抢占式处理任务
典型案例:
- 同一个用户同时触发两次“刷新缓存”,只允许一个线程进入
- 定时任务集群部署时,同一时间只允许一个节点执行清理任务
- 库存扣减、状态迁移、批量结算这类关键动作,在并发场景下需要串行处理
示例:
@Lock4j(keys = "#userId", expire = 30000, acquireTimeout = 1000)
public void refreshUserCache(Long userId) {
}
自动装配链路
1. Spring Boot 自动配置入口
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.njcn.rdms.framework.idempotent.config.RdmsIdempotentConfigurationcom.njcn.rdms.framework.lock4j.config.RdmsLock4jConfigurationcom.njcn.rdms.framework.ratelimiter.config.RdmsRateLimiterConfigurationcom.njcn.rdms.framework.signature.config.RdmsApiSignatureAutoConfiguration
2. 各配置类负责的事情
| 配置类 | 作用 |
|---|---|
RdmsIdempotentConfiguration |
注册幂等切面、幂等 Redis DAO、幂等 KeyResolver |
RdmsLock4jConfiguration |
在 Lock4j 存在时注册默认加锁失败策略 |
RdmsRateLimiterConfiguration |
注册限流切面、限流 Redis DAO、限流 KeyResolver |
RdmsApiSignatureAutoConfiguration |
注册 API 签名切面、签名 Redis DAO |
如何使用
1. 引入依赖
业务模块通常直接依赖:
<dependency>
<groupId>com.njcn</groupId>
<artifactId>rdms-spring-boot-starter-protection</artifactId>
</dependency>
2. 基础前提
这个模块依赖 Redis,因此至少需要可用的 Redis 配置。
其中:
- 幂等依赖
StringRedisTemplate - API 签名依赖
StringRedisTemplate - 限流依赖
RedissonClient - 分布式锁依赖 Lock4j + Redisson
3. 选择合适的保护手段
- 防重复提交,优先使用
@Idempotent - 控制访问频率,优先使用
@RateLimiter - 控制并发进入,优先使用 Lock4j
- 保护开放接口,优先使用
@ApiSignature
不要把它们混成一类能力:
- 幂等不等于分布式锁
- 限流不等于幂等
- API 签名不等于权限校验
4. KeyResolver 的选择建议
- 默认对全局请求做保护,使用默认 Resolver
- 需要按登录用户隔离保护,使用 User Resolver
- 需要按 IP 控制访问,使用 ClientIp Resolver
- 需要按业务字段控制粒度,使用 Expression Resolver
注意事项
- 幂等的
timeout只是保护窗口,不是业务执行超时时间。如果方法执行时间长于这个窗口,后续请求仍可能进入。 - 幂等在异常时默认删除 Key,这是为了允许业务失败后重新提交;如果追求的是“执行期间只允许一个线程进入”,更适合使用分布式锁。
- 限流底层使用 Redisson 的
RRateLimiter,运行环境需要有RedissonClient。 - API 签名依赖 Redis 中预先存在
appId -> appSecret的映射,否则签名校验无法通过。 - API 签名当前实操上应加在方法上,避免把类级注解误认为一定会生效。
- 这个模块的保护能力都是方法级横切逻辑,适合放在 Controller 或 Service 的明确边界上使用,不适合到处滥加。
总结
这个模块的核心价值,是把服务保护相关的高频横切问题统一沉到框架层:
- 如何防止重复提交
- 如何限制单位时间的访问频率
- 如何在分布式环境下串行化关键操作
- 如何校验开放接口的请求签名和防重放
如果把它理解成一个“请求保护与并发保护能力集合”的 starter,这个模块的设计会比较清晰。