防止重復提交,主要是使用鎖的形式來處理,如果是單機部署,可以使用本地緩存鎖(Guava)即可,如果是分布式部署,則需要使用分布式鎖(可以使用zk分布式鎖或者redis分布式鎖),本文的分布式鎖以redis分布式鎖為例。
一、本地鎖(Guava)
1、導入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>21.0</version> </dependency>
2、自定義本地鎖注解
package com.example.demo.utils; import java.lang.annotation.*; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface LocalLock { String key() default ""; //過期時間,使用本地緩存可以忽略,如果使用redis做緩存就需要 int expire() default 5; }
3、本地鎖注解實現
package com.example.demo.utils; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; 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> CACHE = CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(20, TimeUnit.SECONDS).build(); @Around("execution(public * *(..)) && @annotation(com.example.demo.utils.LocalLock)") public Object 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{ return joinPoint.proceed(); }catch (Throwable throwable){ throw new RuntimeException("服務器異常"); }finally { } } private String getKey(String keyExpress, Object[] args){ for (int i = 0; i < args.length; i++) { keyExpress = keyExpress.replace("arg[" + i + "]", args[i].toString()); } return keyExpress; } }
4、控制層
@ResponseBody @PostMapping(value ="/localLock") @ApiOperation(value="重復提交驗證測試--使用本地緩存鎖") @ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")}) @LocalLock(key = "localLock:test:arg[0]") public String localLock(String token){ return "sucess====="+token; }
5、測試
第一次請求:
未過期再次訪問:
二、Redis分布式鎖
1、導入依賴
導入aop依賴和redis依賴即可
2、配置
配置redis連接信息即可
3、自定義分布式鎖注解
package com.example.demo.utils; import java.lang.annotation.*; import java.util.concurrent.TimeUnit; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface CacheLock { //redis鎖前綴 String prefix() default ""; //redis鎖過期時間 int expire() default 5; //redis鎖過期時間單位 TimeUnit timeUnit() default TimeUnit.SECONDS; //redis key分隔符 String delimiter() default ":"; }
4、自定義key規則注解
由於redis的key可能是多層級結構,例如 redistest:demo1:token:kkk這種形式,因此需要自定義key的規則。
package com.example.demo.utils; import java.lang.annotation.*; @Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface CacheParam { String name() default ""; }
5、定義key生成策略接口
package com.example.demo.service; import org.aspectj.lang.ProceedingJoinPoint; import org.springframework.stereotype.Service; public interface CacheKeyGenerator { //獲取AOP參數,生成指定緩存Key String getLockKey(ProceedingJoinPoint joinPoint); }
6、定義key生成策略實現類
package com.example.demo.service.impl; import com.example.demo.service.CacheKeyGenerator; import com.example.demo.utils.CacheLock; import com.example.demo.utils.CacheParam; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; 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; public class CacheKeyGeneratorImp implements CacheKeyGenerator { @Override public String 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 = new StringBuilder(); for(int i=0;i<parameters.length;i++){ final CacheParam cacheParams = parameters[i].getAnnotation(CacheParam.class); //如果屬性不是CacheParam注解,則不處理 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<parameterAnnotations.length;i++){ final Object object = args[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、分布式注解實現
package com.example.demo.utils; import com.example.demo.service.CacheKeyGenerator; 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; @Aspect @Configuration public class CacheLockMethodInterceptor { @Autowired public CacheLockMethodInterceptor(StringRedisTemplate stringRedisTemplate, CacheKeyGenerator cacheKeyGenerator){ this.cacheKeyGenerator = cacheKeyGenerator; this.stringRedisTemplate = stringRedisTemplate; } private final StringRedisTemplate stringRedisTemplate; private final CacheKeyGenerator cacheKeyGenerator; @Around("execution(public * * (..)) && @annotation(com.example.demo.utils.CacheLock)") public Object 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<Boolean>) 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 { return joinPoint.proceed(); } catch (Throwable throwable) { throw new RuntimeException("系統異常"); } } }
8、主函數調整
主函數引入key生成策略
@Bean public CacheKeyGenerator cacheKeyGenerator(){ return new CacheKeyGeneratorImp(); }
9、Controller
@ResponseBody @PostMapping(value ="/cacheLock") @ApiOperation(value="重復提交驗證測試--使用redis鎖") @ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")}) //@CacheLock @CacheLock() public String 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) public String 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
第二次調用: