magic-spring-boot-starter-protection 技术组件,由它的 idempotent 包,提供声明式的幂等特性,可防止重复请求。

// UserController.java
@Idempotent(timeout = 10, timeUnit = TimeUnit.SECONDS, message = "正在添加用户中,请勿重复提交")
@PostMapping("/user/create")
public String createUser(User user){
userService.createUser(user);
return "添加成功";
}
1
2
3
4
5
6
7
2
3
4
5
6
7
添加了注解 Idempotent 后, 表示:每 10 秒钟,只能执行一次【创建用户】操作。
疑问:如果想按照每个用户,或者每个 IP,限制请求呢?
可设置该注解的
keyResolver属性,可选择的有:
- DefaultIdempotentKeyResolver:全局级别
- UserIdempotentKeyResolver:用户级别
- ExpressionIdempotentKeyResolver:自定义级别,通过
keyArg属性指定 Spring EL 表达式
# 1 基本原理
核心逻辑:相同参数的请求,在指定时间窗口内仅允许执行一次。
# 执行流程
- 请求拦截
- 根据方法名 + 参数生成唯一 Redis Key(默认 MD5 压缩,避免过长)。
- 若 Key 已存在 → 直接拒绝请求(防重)。
- 若不存在 → 写入 Redis 并设置过期时间(标记为“执行中”)。
- 执行控制
- 成功:保留 Key 至自动过期(默认不删除)。
- 异常:自动删除 Key,允许重试。
- 超时:若执行时间超过 Key 的过期时间,Redis 自动清理,需合理设置过期时间。
# 2 源码解析

如图,IdempotentAspect 拦截器会拦截带有注解 Idempotent 的方法 。
在执行业务逻辑前,有两个步骤:
1、通过 keyResolvers 解析参数,并组装成 Key 。
/**
* 默认(全局级别)幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key
*
* 为了避免 Key 过长,使用 MD5 进行“压缩”
**/
public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {
@Override
public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs());
return SecureUtil.md5(methodName + argsStr);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
默认(全局级别)幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key, 为了避免 Key 过长,使用 MD5 进行“压缩” 。
2、通过 Redis 的 setNx 命令创建占位
// 1. 尝试获取幂等锁
boolean success = idempotentRedisDAO.setIfAbsent(
appName,
key,
idempotent.timeout(),
idempotent.timeUnit()
);
// 2. 检查是否获取锁成功
if (!success) {
log.info("[幂等拦截][方法({}) 参数({}) 存在重复请求]",
joinPoint.getSignature().toString(),
joinPoint.getArgs());
throw new ServiceException(
GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(),
idempotent.message()
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
setIfAbsent 本质是调用 Redis 的 SETNX 命令。
SETNX 是 Redis 中一个用于设置键值对的命令,全称是 "SET if Not eXists",意思是只有在键不存在的情况下,才会设置该键的值。
如果键已存在,则SETNX 不会执行任何操作,也不会覆盖现有值。
SETNX 常被用来实现分布式锁。当一个进程需要访问一个共享资源时,它可以尝试使用SETNX 来获取锁。如果SETNX 返回1,则表示该进程成功获取了锁;如果返回0,则表示锁已被其他进程持有 。
