第十五节 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秒后则视为正常提交。
四、源码下载
本章节项目源码:点我下载源代码