在开发过程中,前端进行提交后,需要对用户提交进行限制,例如几秒内只能提交一次。本文实现了简易版的后台限制某用户在一定时间段内的提交阈值。
主要思路是对用户提交的参数取出关键字段生成key,利用Redis的setIfAbsent特性,如果键设置成功,则正常进行业务逻辑,键设置失败的话则证明该键存在于redis中,禁止进行业务逻辑。本文基于微服务的具体服务端实现。
需要注意的一点是,在使用微服务的情况下,如果使用非集中的redis,因为负载均衡策略,请求会打到不同的机器上,导致限制失效。此时可参考该思路在Gateway入口处实现限制逻辑
首先定义Redis key的生成策略,基于反射实现
public interface KeyGenerateStrategy {
String generate(ProceedingJoinPoint jp);
}
定义Lock注解,用于Controller使用
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Lock {
/**
* 锁key前缀
* @return
*/
String prefix() default "commit_lock";
/**
* key失效时间
* @return
*/
int expire() default 1;
/**
* key失效默认单位
* @return
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* Key的分隔符
* @return
*/
String splitChar() default ":";
//操作错误提示
String tips() default "系统正在处理请求,请勿重复提交";
}
定义LockField注解,基于ParamEntity实现参数接收,该注解用于entity内的field
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LockField {}
具体实现
@Component
public class SimpleKeyGenerater implements KeyGenerateStrategy {
@Override
public String generate(ProceedingJoinPoint jp) {
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod();
Lock lockAnnotation = method.getAnnotation(Lock.class);
final Object[] args = jp.getArgs();
StringBuilder builder = new StringBuilder();
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = 0; i < parameterAnnotations.length; i++) {
final Object object = args[i];
if (object instanceof BaseRequestParam) {
BaseRequestParam param = (BaseRequestParam)object;
final Field[] fields = param.getRequestInfo().getClass().getDeclaredFields();
Arrays.stream(fields).forEach(field -> {
//获取被LockField注解的参数
final LockField annotation = field.getAnnotation(LockField.class);
if (annotation != null) {
field.setAccessible(true);
builder.append(lockAnnotation.splitChar()).append(ReflectionUtils.getField(field, param.getRequestInfo()));
}
});
} else {
final Field[] fields = object.getClass().getDeclaredFields();
Arrays.stream(fields).forEach(field -> {
//获取被LockField获取的参数
final LockField annotation = field.getAnnotation(LockField.class);
if (annotation != null) {
field.setAccessible(true);
builder.append(lockAnnotation.splitChar()).append(ReflectionUtils.getField(field, object));
}
});
}
}
return lockAnnotation.prefix() + lockAnnotation.splitChar() + DigestUtils.md5Hex(builder.toString());
}
}
定义切面
@Aspect
@Configuration
public class CommmitLockAspect {
private final RedisUtil redisUtil;
private final KeyGenerateStrategy cacheKeyGenerator;
@Autowired
public CommmitLockAspect(RedisUtil redisUtil, KeyGenerateStrategy cacheKeyGenerator) {
this.redisUtil = redisUtil;
this.cacheKeyGenerator = cacheKeyGenerator;
}
@Around("@annotation(xx.xx.xx.live.lock.Lock)") //切入使用Lock注解的函数
public Object interceptor(ProceedingJoinPoint jp) {
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod();
Lock lock = method.getAnnotation(Lock.class);
//生成key
final String lockKey = cacheKeyGenerator.generate(jp);
//setIfAbsent key
final boolean success = redisUtil.setIfAbsent(lockKey,System.currentTimeMillis() + "",lock.expire(),lock.timeUnit());
//如果键已存在,则不执行后续逻辑,返回错误
if (!success) {
return ResponseData.error(ResponseEnum.FORBIDDEN,lock.tips());
}
try {
return jp.proceed();
} catch (Throwable ctx) {
throw new RuntimeException("unknow error");
}
}
}
使用
@Lock(expire = 2, tips = "系统正在处理,请勿频繁点击")
@PostMapping("/createMeetingRoom")
public ResponseData<XXXXXX> createRoom(@Valid @RequestBody AppRequestParam<CreateXXXXParam> param) throws Exception {}
接收request参数的实体类
@Data
@NoArgsConstructor
public class CreatexxxxParam {
@LockField //此处使用LockField注解
private String title;
@LockField
@NotNull(message = "cant be null")
private Integer type;
}