🌟自定义幂等性注解实现(支持分布式锁、EL 表达式、用户维度)
背景介绍
在微服务接口中,为了防止重复提交(例如用户重复点击按钮或网络重试等情况),我们通常需要对接口进行幂等性控制。
常见手段如:Token 机制、数据库唯一索引、分布式锁等。
本方案使用 Redis 实现分布式锁 + AOP 切面 的方式,结合自定义注解 @Idempotent
实现灵活的幂等性控制,支持:
- 方法级幂等
- 用户维度幂等
- Spring EL 表达式自定义 key
- 异常时释放 key 等机制
💡使用方式
@Idempotent(
timeout = 3,
timeUnit = TimeUnit.SECONDS,
message = "请勿重复提交",
keyResolver = UserIdempotentKeyResolver.class
)
@PostMapping("/submit")
public CommonResult<Boolean> submit(@RequestBody SubmitRequest request) {
// 业务处理逻辑
}
🧩注解定义
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
int timeout() default 1;
TimeUnit timeUnit() default TimeUnit.SECONDS;
String message() default "重复请求,请稍后重试";
Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class;
String keyArg() default "";
boolean deleteKeyWhenException() default true;
}
🧵核心逻辑:切面处理 IdempotentAspect
@Aspect
@Slf4j
public class IdempotentAspect {
private final Map<Class<? extends IdempotentKeyResolver>, IdempotentKeyResolver> keyResolvers;
private final IdempotentRedisDAO idempotentRedisDAO;
public IdempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass);
this.idempotentRedisDAO = idempotentRedisDAO;
}
@Around("@annotation(idempotent)")
public Object aroundPointCut(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());
Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");
String key = keyResolver.resolver(joinPoint, idempotent);
boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit());
if (!success) {
log.info("[幂等控制] 方法:{} 参数:{} 被重复调用", joinPoint.getSignature(), joinPoint.getArgs());
throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message());
}
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
if (idempotent.deleteKeyWhenException()) {
idempotentRedisDAO.delete(key);
}
throw throwable;
}
}
}
🔑Key 解析器接口 + 三种实现
1. 默认 Key(方法签名 + 参数)
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);
}
}
2. 用户维度(加 userId + userType)
public class UserIdempotentKeyResolver implements IdempotentKeyResolver {
@Override
public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs());
Long userId = WebFrameworkUtils.getLoginUserId();
Integer userType = WebFrameworkUtils.getLoginUserType();
return SecureUtil.md5(methodName + argsStr + userId + userType);
}
}
3. Spring EL 表达式支持
public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
private final ExpressionParser expressionParser = new SpelExpressionParser();
@Override
public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
Method method = getMethod(joinPoint);
Object[] args = joinPoint.getArgs();
String[] paramNames = parameterNameDiscoverer.getParameterNames(method);
StandardEvaluationContext context = new StandardEvaluationContext();
if (paramNames != null) {
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
}
Expression expression = expressionParser.parseExpression(idempotent.keyArg());
return expression.getValue(context, String.class);
}
private static Method getMethod(JoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
if (!method.getDeclaringClass().isInterface()) {
return method;
}
try {
return point.getTarget().getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
🧊Redis 幂等操作 DAO
@AllArgsConstructor
public class IdempotentRedisDAO {
private static final String IDEMPOTENT = "idempotent:%s";
private final StringRedisTemplate redisTemplate;
public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) {
String redisKey = formatKey(key);
return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit);
}
public void delete(String key) {
String redisKey = formatKey(key);
redisTemplate.delete(redisKey);
}
private static String formatKey(String key) {
return String.format(IDEMPOTENT, key);
}
}
✅适用场景
- 接口幂等性控制(下单、支付、提交等)
- 分布式环境中的重复提交防控
- 限制高并发下的逻辑重复执行
🚀总结
本方案具备以下优势:
- 解耦业务逻辑,只需注解即可开启幂等控制
- 支持灵活的 Key 策略(全局 / 用户 / 表达式)
- 支持 Redis 的分布式锁机制,天然适用于分布式系统
如你也在项目中遇到重复提交等问题,不妨试试自定义注解 + AOP 的方式!
如果你觉得这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、关注我 🧡!