前端防重:

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;
    }
}

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐