当前位置:首页 » 《随便一记》 » 正文

SpringBoot接口+Redis解决用户重复提交问题

12 人参与  2023年03月06日 14:09  分类 : 《随便一记》  评论

点击全文阅读


前言

1. 为什么会出现用户重复提交

网络延迟的情况下用户多次点击submit按钮导致表单重复提交;用户提交表单后,点击【刷新】按钮导致表单重复提交(点击浏览器的刷新按钮,就是把浏览器上次做的事情再做一次,因为这样也会导致表单重复提交);用户提交表单后,点击浏览器的【后退】按钮回退到表单页面后进行再次提交。

2. 重复提交不拦截可能导致的问题

重复数据入库,造成脏数据。即使数据库表有UK索引,该操作也会增加系统的不必要负担;会成为黑客爆破攻击的入口,大量的请求会导致应用崩溃;用户体验差,多条重复的数据还需要一条条的删除等。

3. 解决办法

办法有很多,我这里只说一种,利用Redis的set方法搞定(不是redisson)

项目代码

项目结构

在这里插入图片描述

配置文件

pom.xml

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>2.7.9</version>        <relativePath/> <!-- lookup parent from repository -->    </parent>    <groupId>com.example</groupId>    <artifactId>RequestLock</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>RequestLock</name>    <description>Demo project for Spring Boot</description>    <properties>        <java.version>1.8</java.version>    </properties>    <dependencies>        <!-- redis依赖 -->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-data-redis</artifactId>        </dependency>        <!-- web依赖 -->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <!-- 切面 -->        <dependency>            <groupId>org.aspectj</groupId>            <artifactId>aspectjweaver</artifactId>            <version>1.9.5</version>        </dependency>    </dependencies>    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build></project>

application.properties

spring.application.name=RequestLockserver.port=8080# Redis服务器地址spring.redis.host=127.0.0.1# Redis服务器连接端口spring.redis.port=6379# Redis服务器连接密码(默认为空)spring.redis.password=# 连接池最大连接数(使用负值表示没有限制)spring.redis.jedis.pool.max-active=20# 连接池最大阻塞等待时间(使用负值表示没有限制)spring.redis.jedis.pool.max-wait=-1# 连接池中的最大空闲连接spring.redis.jedis.pool.max-idle=10# 连接池中的最小空闲连接spring.redis.jedis.pool.min-idle=0# 连接超时时间(毫秒)spring.redis.timeout=1000

代码文件

RequestLockApplication.java

package com.example.requestlock;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class RequestLockApplication {    public static void main(String[] args) {        SpringApplication.run(RequestLockApplication.class, args);    }}

User.java

package com.example.requestlock.model;import com.example.requestlock.lock.annotation.RequestKeyParam;public class User {        private String name;    private Integer age;    @RequestKeyParam(name = "phone")    private String phone;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public Integer getAge() {        return age;    }    public void setAge(Integer age) {        this.age = age;    }    public String getPhone() {        return phone;    }    public void setPhone(String phone) {        this.phone = phone;    }    @Override    public String toString() {        return "User{" +                "name='" + name + '\'' +                ", age=" + age +                ", phone='" + phone + '\'' +                '}';    }}

RequestKeyParam.java

package com.example.requestlock.lock.annotation;import java.lang.annotation.*;/** * @description 加上这个注解可以将参数也设置为key,唯一key来源 */@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface RequestKeyParam {    /**     * key值名称     *     * @return 默认为空     */    String name() default "";}

RequestLock.java

package com.example.requestlock.lock.annotation;import java.lang.annotation.*;import java.util.concurrent.TimeUnit;/** * @description 请求防抖锁,用于防止前端重复提交导致的错误 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface RequestLock {    /**     * redis锁前缀     *     * @return 默认为空,但不可为空     */    String prefix() default "";    /**     * redis锁过期时间     *     * @return 默认2秒     */    int expire() default 2;    /**     * redis锁过期时间单位     *     * @return 默认单位为秒     */    TimeUnit timeUnit() default TimeUnit.SECONDS;    /**     * redis  key分隔符     *     * @return 分隔符     */    String delimiter() default ":";}

RequestLockMethodAspect.java

package com.example.requestlock.lock.aspect;import com.example.requestlock.lock.annotation.RequestLock;import com.example.requestlock.lock.keygenerator.RequestKeyGenerator;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisStringCommands;import org.springframework.data.redis.core.RedisCallback;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.types.Expiration;import org.springframework.util.StringUtils;import java.lang.reflect.Method;/** * @description 请求锁切面处理器 */@Aspect@Configurationpublic class RequestLockMethodAspect {    private final StringRedisTemplate stringRedisTemplate;    private final RequestKeyGenerator requestKeyGenerator;    @Autowired    public RequestLockMethodAspect(StringRedisTemplate stringRedisTemplate, RequestKeyGenerator requestKeyGenerator) {        this.requestKeyGenerator = requestKeyGenerator;        this.stringRedisTemplate = stringRedisTemplate;    }    @Around("execution(public * * (..)) && @annotation(com.example.requestlock.lock.annotation.RequestLock)")    public Object interceptor(ProceedingJoinPoint joinPoint) {        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();        Method method = methodSignature.getMethod();        RequestLock requestLock = method.getAnnotation(RequestLock.class);        if (StringUtils.isEmpty(requestLock.prefix())) {//            throw new RuntimeException("重复提交前缀不能为空");            return "重复提交前缀不能为空";        }        //获取自定义key        final String lockKey = requestKeyGenerator.getLockKey(joinPoint);        final Boolean success = stringRedisTemplate.execute(                (RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), new byte[0], Expiration.from(requestLock.expire(), requestLock.timeUnit())                        , RedisStringCommands.SetOption.SET_IF_ABSENT));        if (!success) {//            throw new RuntimeException("您的操作太快了,请稍后重试");            return "您的操作太快了,请稍后重试";        }        try {            return joinPoint.proceed();        } catch (Throwable throwable) {//            throw new RuntimeException("系统异常");            return "系统异常";        }    }}

RequestKeyGenerator.java

package com.example.requestlock.lock.keygenerator;import org.aspectj.lang.ProceedingJoinPoint;/** * 加锁key生成器 */public interface RequestKeyGenerator {    /**     * 获取AOP参数,生成指定缓存Key     *     * @param joinPoint 切入点     * @return 返回key值     */    String getLockKey(ProceedingJoinPoint joinPoint);}

RequestKeyGeneratorImpl.java

package com.example.requestlock.lock.keygenerator.impl;import com.example.requestlock.lock.annotation.RequestKeyParam;import com.example.requestlock.lock.annotation.RequestLock;import com.example.requestlock.lock.keygenerator.RequestKeyGenerator;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.stereotype.Service;import org.springframework.util.ReflectionUtils;import org.springframework.util.StringUtils;import java.lang.annotation.Annotation;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.lang.reflect.Parameter;@Servicepublic class RequestKeyGeneratorImpl implements RequestKeyGenerator {    @Override    public String getLockKey(ProceedingJoinPoint joinPoint) {        //获取连接点的方法签名对象        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();        //Method对象        Method method = methodSignature.getMethod();        //获取Method对象上的注解对象        RequestLock requestLock = method.getAnnotation(RequestLock.class);        //获取方法参数        final Object[] args = joinPoint.getArgs();        //获取Method对象上所有的注解        final Parameter[] parameters = method.getParameters();        StringBuilder sb = new StringBuilder();        for (int i = 0; i < parameters.length; i++) {            final RequestKeyParam cacheParams = parameters[i].getAnnotation(RequestKeyParam.class);            //如果属性不是CacheParam注解,则不处理            if (cacheParams == null) {                continue;            }            //如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam            sb.append(requestLock.delimiter()).append(args[i]);        }        //如果方法上没有加CacheParam注解        if (StringUtils.isEmpty(sb.toString())) {            //获取方法上的多个注解(为什么是两层数组:因为第二层数组是只有一个元素的数组)            final Annotation[][] parameterAnnotations = method.getParameterAnnotations();            //循环注解            for (int i = 0; i < parameterAnnotations.length; i++) {                final Object object = args[i];                //获取注解类中所有的属性字段                final Field[] fields = object.getClass().getDeclaredFields();                for (Field field : fields) {                    //判断字段上是否有CacheParam注解                    final RequestKeyParam annotation = field.getAnnotation(RequestKeyParam.class);                    //如果没有,跳过                    if (annotation == null) {                        continue;                    }                    //如果有,设置Accessible为true(为true时可以使用反射访问私有变量,否则不能访问私有变量)                    field.setAccessible(true);                    //如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam                    sb.append(requestLock.delimiter()).append(ReflectionUtils.getField(field, object));                }            }        }        //返回指定前缀的key        return requestLock.prefix() + sb;    }}

UserController.java

package com.example.requestlock.controller;import com.example.requestlock.lock.annotation.RequestLock;import com.example.requestlock.model.User;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/user")public class UserController {    @PostMapping("/addUser1")    public String addUser1(@RequestBody User user) {        System.out.println("不做任何处理" + user);        return "添加成功";    }    @PostMapping("/addUser2")    @RequestLock(prefix = "addUser")    public String addUser2(@RequestBody User user) {        System.out.println("防重提交" + user);        return "添加成功";    }}

效果展示

调用addUser1接口

在这里插入图片描述
这里无论点击多少次提交,都会展示添加“添加成功”,这样是不行的。

调用addUser2接口

在这里插入图片描述
第一次提交,“添加成功”。
在这里插入图片描述
快速点击第二次提交,就会出现“您的操作太快了,请稍后重试”提示。

原理解释

该RequestLock(请求锁)利用了Redis的单线程处理以及Key值过期特点,核心通过RequestLock、RequestKeyParam注解生成一个唯一的key值,存入redis后设置一个过期时间(1-3秒),当第二次请求的时候,判断生成的key值是否在Redis中存在,如果存在则认为第二次提交是重复的。


点击全文阅读


本文链接:http://zhangshiyu.com/post/53963.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1