判断页面是否有未提交的内容_防止表单重复提交看这里!!!

0ff3d857799378d741f75ae4ede3de59.png

         要解决重复提交这事,先要知道什么是重复提交

假如用户的网速慢,用户点击提交按钮,却因为网速慢,而没有跳转到新的页面,这时的用户会再次点击提交按钮,举个例子:用户点击订单页面,当点击提交按钮的时候,也许因为网速的原因,没有跳转到新的页面,这时的用户会再次点击提交按钮,如果没有经过处理的话,这时用户就会生成两份订单,类似于这种场景都叫重复提交。

好了,为了解决这个问题,可以马上想到通过前端将提交按钮点击后设置倒计时30秒不能再点击就行啊,这些方案是前端的,我们主要说后端方案,看下面的图。防止重复提交后端就是对提交内容加锁即可,判断如果冲突提交就舍弃提交内容,如果没重复就执行后续业务方法。别一提到锁就看不懂,这里的锁你可以理解成唯一值

8ea9594cd24490272724de74ee665787.png

秉承一贯作风,不讲故事就是干,通过代码理解,先搭建SpringBoot项目

pom坐标

    <parent>        <groupId>org.springframework.bootgroupId>        <artifactId>spring-boot-starter-parentartifactId>        <version>2.1.6.RELEASEversion>    parent>    <dependencies>        <dependency>            <groupId>org.springframework.bootgroupId>            <artifactId>spring-boot-starter-webartifactId>        dependency>        <dependency>            <groupId>org.springframework.bootgroupId>            <artifactId>spring-boot-starter-aopartifactId>        dependency>        <dependency>            <groupId>com.google.guavagroupId>            <artifactId>guavaartifactId>            <version>21.0version>        dependency>    dependencies>

入口类

@SpringBootApplicationpublic class UserApplication {    public static void main(String[] args) {        SpringApplication.run(UserApplication.class, args);    }}

很普通的自定义注解@LocalLock

/** *  单机防止重复提交注解 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface LocalLock {    String key() default "";}

LockMethodInterceptor拦截器类

package cn.wfb.demo.local;import com.google.common.cache.Cache;import com.google.common.cache.CacheBuilder;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.util.StringUtils;import java.lang.reflect.Method;import java.util.concurrent.TimeUnit;/** *  思路是通过aop的方式拦截用户所有请求,前端需要提交唯一的token值 *后端拦截所有请求将token与Cache缓存内容进行比较,如果token存在就抛异常,不存在直接缓存到Cache中 */@Aspect@Configurationpublic class LockMethodInterceptor {    /**     * 通过CacheBuilder设置缓存个数,设置缓存过期时间     */    private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder()            // 最大缓存 1000 个            .maximumSize(1000)            // 设置写缓存后 5 秒钟过期            .expireAfterWrite(5, TimeUnit.SECONDS)            .build();    //所有请求并且带有@LocalLock注解的,进行拦截    @Around("execution(public * *(..)) && @annotation(cn.wfb.demo.local.LocalLock)")    public Object interceptor(ProceedingJoinPoint pjp) {        MethodSignature signature = (MethodSignature) pjp.getSignature();        Method method = signature.getMethod();        LocalLock localLock = method.getAnnotation(LocalLock.class);        String key = getKey(localLock.key(), pjp.getArgs());                //如果该key已经在缓存中,那么抛异常,不存在则存入缓存中        if (!StringUtils.isEmpty(key)) {            if (CACHES.getIfPresent(key) != null) {                throw new RuntimeException("请勿重复请求");            }            // 如果是第一次请求,就将 key 当前对象压入缓存中            CACHES.put(key, key);        }        try {            return pjp.proceed();        } catch (Throwable throwable) {            throw new RuntimeException("服务器异常");        }    }    /**     *     * 本例中传入的key是user:arg[0],被替换参数名user:[token参数内容]     * 两个参数的话user:arg[0],user:arg[1],user:可以省略只为了标识意义明确     *     * @param keyExpress 表达式:例如user:arg[0]     * @param args       参数     * @return 生成的key     */    private String getKey(String keyExpress, Object[] args) {        for (int i = 0; i < args.length; i++) {            //将arg[0]替换成参数的内容            keyExpress = keyExpress.replace("arg[" + i + "]", args[i].toString());        }        return keyExpress;    }}

Controller

@RestController@RequestMapping("/user")public class UserController {    /**     * 通过加本地锁防止重复提交     */    @LocalLock(key = "user:arg[0]")    @GetMapping("/local")    public Map queryLocal(@RequestParam String token) {        HashMap map = new HashMap();        map.put("success", token);        return map;    }}

测试localhost:8080/user/local?token=‘唯一ID值’  其实整体思路很简单,这个token参数的值应该让前端通过工具类产生唯一值之后提交。提交到后端通过AOP拦截请求,将token内容先判断是否已存在Cache(google的工具类)的缓存,不存在则缓存到Cache中,如果已存在直接抛异常。整体代码很好理解。

那么为什么说分布式系统要和传统的方式区别开,这里的原因大家应该能明白就是缓存位置问题,如果传统项目用本地缓存保存即可,但是分布式就需要Redis作为共享内存,道理是一样的代码都类似,只需要将本例中google的本地Cache缓存改成redis保存即可

采用原生 API 来实现Redis的分布式锁代码如下

//connection.set(前端token的字节,字节数组,通过Expiration设置key时间,使用的Redis的指令是SET_IF_ABSENT)final Boolean success = redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), new byte[0], Expiration.from(lock.expire(), lock.timeUnit()), RedisStringCommands.SetOption.SET_IF_ABSENT));

其他想问的也可以进群聊

b4a6828a5adfff9b40648196c3925090.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值