使用redis实现防重-数据重复提交
前端防重:1.请求完之后,按钮置灰,等到返回结果再修改2.请求完之后进行重定向到成功页面(防止刷新提交和按钮重复提交)后端防重1.幂等性操作,使用数据库设置唯一标识来解决,相当于把防重的操作交给了数据库,来处理,这种方式避免了垃圾数据入库,表单重复提交问题,但是增加了数据库的压力一定程度来书是不太合理的,唯一性是数据库本生该有的设计,这个一般作为最后一道防线。2.session方法实现,每次提交一
前端防重:
1.请求完之后,按钮置灰,等到返回结果再修改
2.请求完之后进行重定向到成功页面(防止刷新提交和按钮重复提交)
后端防重
1.幂等性操作,使用数据库设置唯一标识来解决,相当于把防重的操作交给了数据库,来处理,这种方式避免了垃圾数据入库,表单重复提交问题,但是增加了数据库的压力一定程度来书是不太合理的,唯一性是数据库本生该有的设计,这个一般作为最后一道防线。
2.session方法实现,每次提交一次请求前(或者登录时候生成一个,然后后面每次提交一次就使用,再生成一个返回,这样保证用户的session都有这一个标识。),先调用后端接口获取到一个唯一标识,这个标识前端session隐藏,等到提交表单时候传入这个标识,之后清楚session,如果重复提交,校验唯一标识存在则保存失败
别人解释:
在服务器端,生成一个唯一的标识符,将它存入session,同时将它写入表单的隐藏字段中,然后将表单页面发给浏览器,用户录入信息后点击提交,在服务器端,获取表单中隐藏字段的值,与session中的唯一标识符比较,相等说明是首次提交,就处理本次请求,然后将session中的唯一标识符移除;不相等说明是重复提交,就不再处理。
3.直接加锁,为了避免短时间的重复提交,使用。这种方式只能在限制固定时间内的操作,当然最好还得加数据库唯一标识作为兜底
我最终的解决方案是使用redis实现:key生成策略(怎么保证唯一很重要),存活时间,缓存策略(redis还是本地map)具体代码如下:
注解Resubmit
/**
* 防重复提交拦截的注解
* @author ggy
* @date 2021/12/9
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Resubmit {
/**
* 默认提交的0-表示前一个请求执行完后面一个就可以继续请求
* 如果是10 表示前一个请求执行开始10秒之后 后面一个就可以才能请求
* @return
*/
int delaySeconds() default 0;
/**
* 唯一key字段 取自请求参数上的字段字段,可以直接是字段值,比如userId
* 如果不传取第一个参数md5 加密生成唯一key
*/
String uniqueKeyField() default "";
}
使用的aop切面
/**
* @ClassName ResubmitAspect
* @Decription 重复提交切面
* @Author ggy
* @Date 2021/12/9 5:38 下午
*/
@Component
@Aspect
@Slf4j
public class ResubmitAspect {
/**
* redis目录分割符
*/
private final String CATALOG_SEPARATOR = ":";
@Value("${spring.profiles.active}")
private String springProfilesActive;
@Value("${spring.application.name}")
private String springApplicationName;
@Resource
RedissonClient redissonClient;
/**
* 重复提交切点
*/
@Pointcut("@annotation(xxx.aop.anno.Resubmit)")
public void resubmitPointcut() {
}
/**
* 执行拦截
*
* @param joinPoint 切点
* @return
*/
@Around("resubmitPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
//获取这个上面的注解
Method method = methodSignature.getMethod();
Resubmit annotation = method.getAnnotation(Resubmit.class);
int delaySeconds = annotation.delaySeconds();
//获取第一个参数值
String redisKey = getLockKey(signature, annotation, methodSignature, joinPoint.getArgs());
RLock lock = redissonClient.getLock(redisKey);
try {
boolean lockFlag = true;
if (delaySeconds == 0) {
//这种表示一直在续约 默认有效期为30秒 之后每10秒更新一次为30秒,直到任务完成
lockFlag = lock.tryLock(0, -1, TimeUnit.SECONDS);
} else {
lockFlag = lock.tryLock(0, delaySeconds, TimeUnit.SECONDS);
}
if (!lockFlag) {
return Response.error(ErrorCode.EXCEPTION.getCode(), "请勿重复操作~", null);
}
log.info("获取锁成功");
return joinPoint.proceed();
} catch (InterruptedException e) {
//获取redis锁导致的报错
log.error("ResubmitAspect/Exception:[{}]", e.getMessage());
return Response.error(ErrorCode.EXCEPTION.getCode(), e.getMessage(), null);
} finally {
//如果设置了过期时间就让它自动过期
if (delaySeconds == 0) {
if (lock.isHeldByCurrentThread()) {
// 解锁
log.info("解锁成功 {}", redisKey);
lock.unlock();
} else {
log.info("获取锁失败 {}", redisKey);
}
}
}
}
/**
* 获取唯一标识key
*
* @param signature
* @param annotation
* @param methodSignature
* @param args
* @return
*/
private String getLockKey(Signature signature, Resubmit annotation, MethodSignature methodSignature, Object[] args) {
//获取包含方法参数的值
String keyField = annotation.uniqueKeyField();
String key = "";
if (StringUtils.isEmpty(keyField)) {
//如果没有传值,直接取第一个值,进行MD5加密作为key
Object firstArg = args[0];
key = MD5Util.getMD5(JSONObject.toJSONString(firstArg), "");
} else {
String[] parameterNames = methodSignature.getParameterNames();
for (int index = 0; index < parameterNames.length; index++) {
if (keyField.equals(parameterNames[index])) {
Object obj = args[index];
key = JSONObject.toJSONString(obj);
break;
}
}
if (StringUtils.isEmpty(key)) {
throw new Exception(ErrorCode.EXCEPTION.getCode(), "请设置正确的Resubmit唯一key标识");
}
}
//获取类名称,方法名称
String methodName = signature.getName();
String simpleClassName = signature.getDeclaringType().getSimpleName();
//项目名称 + 环境编码 + 获取类名称 + 方法名称 + 唯一key
key = springApplicationName + CATALOG_SEPARATOR + springProfilesActive + CATALOG_SEPARATOR + simpleClassName + CATALOG_SEPARATOR + methodName + CATALOG_SEPARATOR + key;
return key;
}
}
更多推荐
所有评论(0)