防止重复提交,主要是使用锁的形式来处理,如果是单机部署,可以使用本地缓存锁(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、测试

  第一次请求:

  SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)-LMLPHP

  未过期再次访问:

  SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)-LMLPHP

二、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前缀,因此会报错

SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)-LMLPHP

  (2)没有加CacheParam注解

  第一次调用:

SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)-LMLPHP

  缓存信息:

  可以发现key为prifix的值

SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)-LMLPHP

  第二次调用:

SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)-LMLPHP

  (3)增加了CacheParam注解

  第一次调用:

  SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)-LMLPHP

  缓存信息:

  可以发现缓存的内容为prefix+@CacheParam

  SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)-LMLPHP

  第二次调用:

SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)-LMLPHP

05-22 13:51