防止重复提交,主要是使用锁的形式来处理,如果是单机部署,可以使用本地缓存锁(Guava)即可,如果是分布式部署,则需要使用分布式锁(可以使用zk分布式锁或者redis分布式锁),本文的分布式锁以redis分布式锁为例。
一、本地锁(Guava)
1、导入依赖
org.springframework.boot
spring-boot-starter-aop
com.google.guava
guava
21.0
2、自定义本地锁注解
packagecom.example.demo.utils;import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inheritedpublic @interfaceLocalLock {
String key()default "";//过期时间,使用本地缓存可以忽略,如果使用redis做缓存就需要
int expire() default 5;
}
3、本地锁注解实现
packagecom.example.demo.utils;importcom.google.common.cache.Cache;importcom.google.common.cache.CacheBuilder;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.Signature;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.reflect.MethodSignature;importorg.springframework.context.annotation.Configuration;importorg.springframework.util.StringUtils;importjava.lang.reflect.Method;importjava.util.concurrent.TimeUnit;
@Aspect
@Configurationpublic classLockMethodInterceptor {//定义缓存,设置最大缓存数及过期日期
private static final Cache CACHE = CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(20, TimeUnit.SECONDS).build();
@Around("execution(public * *(..)) && @annotation(com.example.demo.utils.LocalLock)")publicObject interceptor(ProceedingJoinPoint joinPoint){
MethodSignature signature=(MethodSignature) joinPoint.getSignature();
Method method=signature.getMethod();
LocalLock localLock= method.getAnnotation(LocalLock.class);
String key=getKey(localLock.key(),joinPoint.getArgs());if(!StringUtils.isEmpty(key)){if(CACHE.getIfPresent(key) != null){throw new RuntimeException("请勿重复请求!");
}
CACHE.put(key,key);
}try{returnjoinPoint.proceed();
}catch(Throwable throwable){throw new RuntimeException("服务器异常");
}finally{
}
}privateString getKey(String keyExpress, Object[] args){for (int i = 0; i < args.length; i++) {
keyExpress= keyExpress.replace("arg[" + i + "]", args[i].toString());
}returnkeyExpress;
}
}
4、控制层
@ResponseBody
@PostMapping(value="/localLock")
@ApiOperation(value="重复提交验证测试--使用本地缓存锁")
@ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})
@LocalLock(key= "localLock:test:arg[0]")publicString localLock(String token){return "sucess====="+token;
}
5、测试
第一次请求:
未过期再次访问:
二、Redis分布式锁
1、导入依赖
导入aop依赖和redis依赖即可
2、配置
配置redis连接信息即可
3、自定义分布式锁注解
packagecom.example.demo.utils;import java.lang.annotation.*;importjava.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inheritedpublic @interfaceCacheLock {//redis锁前缀
String prefix() default "";//redis锁过期时间
int expire() default 5;//redis锁过期时间单位
TimeUnit timeUnit() defaultTimeUnit.SECONDS;//redis key分隔符
String delimiter() default ":";
}
4、自定义key规则注解
由于redis的key可能是多层级结构,例如 redistest:demo1:token:kkk这种形式,因此需要自定义key的规则。
packagecom.example.demo.utils;import java.lang.annotation.*;
@Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inheritedpublic @interfaceCacheParam {
String name()default "";
}
5、定义key生成策略接口
packagecom.example.demo.service;importorg.aspectj.lang.ProceedingJoinPoint;importorg.springframework.stereotype.Service;public interfaceCacheKeyGenerator {//获取AOP参数,生成指定缓存Key
String getLockKey(ProceedingJoinPoint joinPoint);
}
6、定义key生成策略实现类
packagecom.example.demo.service.impl;importcom.example.demo.service.CacheKeyGenerator;importcom.example.demo.utils.CacheLock;importcom.example.demo.utils.CacheParam;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.reflect.MethodSignature;importorg.springframework.util.ReflectionUtils;importorg.springframework.util.StringUtils;importjava.lang.annotation.Annotation;importjava.lang.reflect.Field;importjava.lang.reflect.Method;importjava.lang.reflect.Parameter;public class CacheKeyGeneratorImp implementsCacheKeyGenerator {
@OverridepublicString getLockKey(ProceedingJoinPoint joinPoint) {//获取连接点的方法签名对象
MethodSignature methodSignature =(MethodSignature) joinPoint.getSignature();//Method对象
Method method =methodSignature.getMethod();//获取Method对象上的注解对象
CacheLock cacheLock = method.getAnnotation(CacheLock.class);//获取方法参数
final Object[] args =joinPoint.getArgs();//获取Method对象上所有的注解
final Parameter[] parameters =method.getParameters();
StringBuilder sb= newStringBuilder();for(int i=0;i
if(cacheParams == null){continue;
}//如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam
sb.append(cacheLock.delimiter()).append(args[i]);
}//如果方法上没有加CacheParam注解
if(StringUtils.isEmpty(sb.toString())){//获取方法上的多个注解(为什么是两层数组:因为第二层数组是只有一个元素的数组)
final Annotation[][] parameterAnnotations =method.getParameterAnnotations();//循环注解
for(int i=0;i
final Field[] fields =object.getClass().getDeclaredFields();for(Field field : fields){//判断字段上是否有CacheParam注解
final CacheParam annotation = field.getAnnotation(CacheParam.class);//如果没有,跳过
if(annotation ==null){continue;
}//如果有,设置Accessible为true(为true时可以使用反射访问私有变量,否则不能访问私有变量)
field.setAccessible(true);//如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam
sb.append(cacheLock.delimiter()).append(ReflectionUtils.getField(field,object));
}
}
}//返回指定前缀的key
return cacheLock.prefix() +sb.toString();
}
}
7、分布式注解实现
packagecom.example.demo.utils;importcom.example.demo.service.CacheKeyGenerator;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.reflect.MethodSignature;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.connection.RedisStringCommands;importorg.springframework.data.redis.core.RedisCallback;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.data.redis.core.types.Expiration;importorg.springframework.util.StringUtils;importjava.lang.reflect.Method;
@Aspect
@Configurationpublic classCacheLockMethodInterceptor {
@AutowiredpublicCacheLockMethodInterceptor(StringRedisTemplate stringRedisTemplate, CacheKeyGenerator cacheKeyGenerator){this.cacheKeyGenerator =cacheKeyGenerator;this.stringRedisTemplate =stringRedisTemplate;
}private finalStringRedisTemplate stringRedisTemplate;private finalCacheKeyGenerator cacheKeyGenerator;
@Around("execution(public * * (..)) && @annotation(com.example.demo.utils.CacheLock)")publicObject interceptor(ProceedingJoinPoint joinPoint){
MethodSignature methodSignature=(MethodSignature) joinPoint.getSignature();
Method method=methodSignature.getMethod();
CacheLock cacheLock= method.getAnnotation(CacheLock.class);if(StringUtils.isEmpty(cacheLock.prefix())){throw new RuntimeException("前缀不能为空");
}//获取自定义key
final String lockkey =cacheKeyGenerator.getLockKey(joinPoint);final Boolean success =stringRedisTemplate.execute(
(RedisCallback) connection -> connection.set(lockkey.getBytes(), new byte[0], Expiration.from(cacheLock.expire(), cacheLock.timeUnit())
, RedisStringCommands.SetOption.SET_IF_ABSENT));if (!success) {//TODO 按理来说 我们应该抛出一个自定义的 CacheLockException 异常;这里偷下懒
throw new RuntimeException("请勿重复请求");
}try{returnjoinPoint.proceed();
}catch(Throwable throwable) {throw new RuntimeException("系统异常");
}
}
}
8、主函数调整
主函数引入key生成策略
@BeanpublicCacheKeyGenerator cacheKeyGenerator(){return newCacheKeyGeneratorImp();
}
9、Controller
@ResponseBody
@PostMapping(value="/cacheLock")
@ApiOperation(value="重复提交验证测试--使用redis锁")
@ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})//@CacheLock
@CacheLock()publicString cacheLock(String token){return "sucess====="+token;
}
@ResponseBody
@PostMapping(value="/cacheLock1")
@ApiOperation(value="重复提交验证测试--使用redis锁")
@ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})//@CacheLock
@CacheLock(prefix = "redisLock.test",expire = 20)publicString cacheLock1(String token){return "sucess====="+token;
}
@ResponseBody
@PostMapping(value="/cacheLock2")
@ApiOperation(value="重复提交验证测试--使用redis锁")
@ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})//@CacheLock
@CacheLock(prefix = "redisLock.test",expire = 20)public String cacheLock2(@CacheParam(name = "token") String token){return "sucess====="+token;
}
10、测试
(1)由于cacheLock方法的CacheLock注解没有加prefix前缀,因此会报错
(2)没有加CacheParam注解
第一次调用:
缓存信息:
可以发现key为prifix的值
第二次调用:
(3)增加了CacheParam注解
第一次调用:
缓存信息:
可以发现缓存的内容为prefix+@CacheParam
第二次调用: