限制请求频率idempotent
请求幂等idempotent
在后端接口开发好之后,对于下单,支付等一些请求,防止重复快速请求,生成重复数据,可以做请求幂等的限制,但如何优雅的做出有效的限制,还不会导致代码复杂化,需要更多的研究,下面是工作中项目使用的方案(注解+Reids分布式锁)。
注解可以更灵活的使用功能,不会导致代码复杂化
代码结构
|-- idempotent
|-- config
|-- IdempotentConfiguration.class
|-- core
|-- annotation
|-- Idempotent.class
|-- aop
|-- IdempotentAspect.class
|-- keyresolver
|-- impl
|-- DefaultIdempotentKeyResolver.class
|-- ExpressionIdempotentKeyResolver.class
|-- UserIdempotentKeyResolver.class
|-- IdempotentKeyResolver.class
|-- redis
|--IdempotentRedisDAO.classconfig
package com.chemical.framework.idempotent.config;
import com.chemical.framework.idempotent.core.aop.IdempotentAspect;
import com.chemical.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import com.chemical.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver;
import com.chemical.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
import com.chemical.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver;
import com.chemical.framework.idempotent.core.redis.IdempotentRedisDAO;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.List;
@AutoConfiguration(after = RedisAutoConfiguration.class)
public class IdempotentConfiguration {
@Bean
public IdempotentAspect idempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
return new IdempotentAspect(keyResolvers, idempotentRedisDAO);
}
@Bean
public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) {
return new IdempotentRedisDAO(stringRedisTemplate);
}
// ========== 各种 IdempotentKeyResolver Bean ==========
@Bean
public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() {
return new DefaultIdempotentKeyResolver();
}
@Bean
public UserIdempotentKeyResolver userIdempotentKeyResolver() {
return new UserIdempotentKeyResolver();
}
@Bean
public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() {
return new ExpressionIdempotentKeyResolver();
}
}Idempotent
注解的定义
package com.chemical.framework.idempotent.core.annotation;
import com.chemical.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import com.chemical.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver;
import com.chemical.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
import com.chemical.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* 幂等注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/**
* 幂等的超时时间,默认为 1 秒
* <p>
* 注意,如果执行时间超过它,请求还是会进来
*/
int timeout() default 1;
/**
* 时间单位,默认为 SECONDS 秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 提示信息,正在执行中的提示
*/
String message() default "重复请求,请稍后重试";
/**
* 使用的 Key 解析器
*
* @see DefaultIdempotentKeyResolver 全局级别
* @see UserIdempotentKeyResolver 用户级别
* @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算
*/
Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class;
/**
* 使用的 Key 参数
*/
String keyArg() default "";
/**
* 删除 Key,当发生异常时候
* <p>
* 问题:为什么发生异常时,需要删除 Key 呢?
* 回答:发生异常时,说明业务发生错误,此时需要删除 Key,避免下次请求无法正常执行。
* <p>
* 问题:为什么不搞 deleteWhenSuccess 执行成功时,需要删除 Key 呢?
* 回答:这种情况下,本质上是分布式锁,推荐使用 @Lock4j 注解
*/
boolean deleteKeyWhenException() default true;
}IdempotentAspect
AOP切面定义,主要的逻辑是根据给的keyResolver去解析KEY,keyResolver的用途是为了注解可以提供更多的场景,让代码不会太冗杂,随后根据Reids操作,去查看是否存在KEY,设置成功则放行,设置失败则报错提示请求重复
package com.chemical.framework.idempotent.core.aop;
import com.chemical.framework.common.exception.ServiceException;
import com.chemical.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.chemical.framework.common.util.collection.CollectionUtils;
import com.chemical.framework.idempotent.core.annotation.Idempotent;
import com.chemical.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import com.chemical.framework.idempotent.core.redis.IdempotentRedisDAO;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.util.Assert;
import java.util.List;
import java.util.Map;
/**
* 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作
*/
@Aspect
@Slf4j
public class IdempotentAspect {
/**
* IdempotentKeyResolver 集合
*/
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(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
if (idempotent.deleteKeyWhenException()) {
idempotentRedisDAO.delete(key);
}
throw throwable;
}
}
}
IdempotentKeyResolver
KEY解析接口,提供三种实现,按场景使用,或者有新场景可以更方便的添加KEY的解析方式
package com.chemical.framework.idempotent.core.keyresolver;
import com.chemical.framework.idempotent.core.annotation.Idempotent;
import org.aspectj.lang.JoinPoint;
/**
* 幂等 Key 解析器接口
*/
public interface IdempotentKeyResolver {
/**
* 解析一个 Key
*
* @param idempotent 幂等注解
* @param joinPoint AOP 切面
* @return Key
*/
String resolver(JoinPoint joinPoint, Idempotent idempotent);
}
DefaultIdempotentKeyResolver
package com.chemical.framework.idempotent.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.chemical.framework.idempotent.core.annotation.Idempotent;
import com.chemical.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import org.aspectj.lang.JoinPoint;
/**
* 默认(全局级别)幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key
* <p>
* 为了避免 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);
}
}
ExpressionIdempotentKeyResolver
package com.chemical.framework.idempotent.core.keyresolver.impl;
import cn.hutool.core.util.ArrayUtil;
import com.chemical.framework.idempotent.core.annotation.Idempotent;
import com.chemical.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
/**
* 基于 Spring EL 表达式,
*/
public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {
private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
private final ExpressionParser expressionParser = new SpelExpressionParser();
@Override
public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
// 获得被拦截方法参数名列表
Method method = getMethod(joinPoint);
Object[] args = joinPoint.getArgs();
String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);
// 准备 Spring EL 表达式解析的上下文
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
if (ArrayUtil.isNotEmpty(parameterNames)) {
for (int i = 0; i < parameterNames.length; i++) {
evaluationContext.setVariable(parameterNames[i], args[i]);
}
}
// 解析参数
Expression expression = expressionParser.parseExpression(idempotent.keyArg());
return expression.getValue(evaluationContext, 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(
point.getSignature().getName(), method.getParameterTypes());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
UserIdempotentKeyResolver
package com.chemical.framework.idempotent.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.chemical.framework.idempotent.core.annotation.Idempotent;
import com.chemical.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
import com.chemical.framework.web.core.util.WebFrameworkUtils;
import org.aspectj.lang.JoinPoint;
/**
* 用户级别的幂等 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key
* <p>
* 为了避免 Key 过长,使用 MD5 进行“压缩”
*/
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);
}
}IdempotentRedisDAO
Redis方法封装
package com.chemical.framework.idempotent.core.redis;
import lombok.AllArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* 幂等 Redis DAO
*/
@AllArgsConstructor
public class IdempotentRedisDAO {
/**
* 幂等操作
* <p>
* KEY 格式:idempotent:%s // 参数为 uuid
* VALUE 格式:String
* 过期时间:不固定
*/
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);
}
}
限制请求频率idempotent
http://example.com/2025/02/24/限制请求频率idempotent/