第十五节 SpringBoot使用本地锁

一、问题现象

        前端小姐姐对我说过,需要后台限制重复表单提交。前端小姐姐把form表单数据提交到后台,但是在推送的时候,可能因为网络延迟,多点了两下提交按钮。怎么才能解决表单重复提交的问题呢?

        根据以前的老经验,一般在数据库表中为某个提交字段创建唯一索引,这样就能限制相同数据入库。

        上述经验已经老的掉牙,每次都在数据库中创建唯一索引。为什么非要等到入库的时候在做限制呢?

        我期望在Controller层就干掉这种事情。

        当然,本地锁只适用于项目单节点部署,不适用于分布式部署。

二、设计与编码

         大体思路为,在某个Controller上增加一个注解,然后使用AOP扫描使用了这个注解的方法,织入环绕通知。

         在环绕通知里面检查是否是同一批请求参数,如果是同一批请求参数,那么就抛出异常来限制执行实际业务。

         需要导入的依赖

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>21.0</version>
        </dependency>

        <!-- aop -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

         (1)不多BB,第一步,开发我们的注解。

           注解设计为用在方法上,且可通过反射来获取注解上配置的值。

package com.zhoutianyu.learnspringboot.lock.single;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LocalLock {

    String key();
}

       (2)编写切面。

大体思路:

         首先编写切面类,使用@Aspect与@Configuration标注是一个切面类,并交由IOC容器管理。

         接着,创建一个缓存,类似于HashMap这样的键值对。限制表单提交的相同的数据5秒内不可重复提交。

         然后,使用@Around来编写具体的环绕通知。扫描每个使用了@LocalLock的方法。

         最后就是具体的业务。先通过切面得到方法的签名与参数。通过方法签名与方法参数。构造出来一个key。个人设计 key为:“类名+方法名+方法参数”。

         表单两次重复提交,那么它们的两次请求,我们将接收相同的参数,那么生成的key将会是同一个。将这个key存放到缓存中。缓存存放时间为5秒,即5秒内重复提交的参数会抛出异常。这样的话在业务执行前就解决了重复提交的问题。

package com.zhoutianyu.learnspringboot.lock.single;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.zhoutianyu.learnspringboot.exception.FormRepeatException;
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.cache.interceptor.KeyGenerator;
import org.springframework.cache.interceptor.SimpleKeyGenerator;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

@Aspect
@Configuration
public class LockMethodInterceptor {

    private static final Cache<String, Object> CACHES =
            CacheBuilder.newBuilder().maximumSize(100).expireAfterWrite(5, TimeUnit.SECONDS).build();

    @Around(value = "execution(public * *(..)) 
                 && @annotation(com.zhoutianyu.learnspringboot.lock.single.LocalLock)")
    public Object interceptor(ProceedingJoinPoint pjp) throws Exception {
        //get Method by aop
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        //generate a key by Method
        String key = generateKey(pjp, method);

        if (!StringUtils.isEmpty(key)) {
            if (CACHES.getIfPresent(key) != null) {
                //if key exists in caches
                throw new FormRepeatException("重复提交");
            } else {
                //put key into caches when first time commit
                CACHES.put(key, key);
            }
        }

        //normal business
        try {
            Object[] args = pjp.getArgs();
            return pjp.proceed(args);
        } catch (Throwable throwable) {
            throw new Exception("服务器内部错误");
        }
    }

    /**
     * generate a key by Method sign
     * key : className + functionName + args
     */
    private String generateKey(ProceedingJoinPoint pjp, Method method) {
        KeyGenerator keyGenerator = new SimpleKeyGenerator();
        LocalLock localLock = method.getAnnotation(LocalLock.class);
        return localLock.key() + 
                 keyGenerator.generate(pjp.getTarget(), method, pjp.getArgs());
    }
}

        (3)最后编写一个Controller,在方法上添加我们的注解@LocalLock。注解的key值期望用户输入为“类名 + 方法名”。

package com.zhoutianyu.learnspringboot.lock.single;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LocalLockController {

    private static final Logger LOGGER = LoggerFactory.getLogger(LocalLockController.class);

    @GetMapping(value = "/localLock")
    @LocalLock(key = "LocalLockController.function")
    public String function(String paramType) {
        LOGGER.info("paramType is {}", paramType);
        return paramType;
    }
}

三、测试

        对了,上面的代码忘了说,如果出现表单重复提交,那么抛出异常。

        我这里FormRepeatException的异常码设计为 -2,并由全局异常拦截器拦截并处理,最后响应给前端小姐姐。

       

       5秒内重复请求,则视为表单重复提交。5秒后则视为正常提交。

       

四、源码下载

        本章节项目源码:点我下载源代码

        目录贴:跟着大宇学SpringBoot-------目录帖

 

posted @ 2022-07-17 12:14  小大宇  阅读(827)  评论(0编辑  收藏  举报