怎么实现呢:进入正题
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.6.8version>
dependency>
<dependency>
<groupId>org.apache.tomcat.embedgroupId>
<artifactId>tomcat-embed-coreartifactId>
<version>9.0.21version>
dependency>
<dependency>
<groupId>org.springframework.datagroupId>
<artifactId>spring-data-redisartifactId>
<version>2.6.3version>
dependency>
/**
* 自定义注解防止表单重复提交
* @author Yuan Haozhe
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
}
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 该加锁方法仅针对单实例 Redis 可实现分布式加锁
* 对于 Redis 集群则无法使用
*
* 支持重复,线程安全
*
* @param lockKey 加锁键
* @param clientId 加锁客户端唯一标识(采用UUID)
* @param seconds 锁过期时间
* @return
*/
public boolean tryLock(String lockKey, String clientId, long seconds) {
//
return redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, seconds, TimeUnit.SECONDS);
}
}
对参数进行介绍:
第一个为key,我们使用key来当锁,因为key是唯一的。我们传的是lockKey
第二个为value,通过给value赋值为clientId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。clientId可以使用UUID.randomUUID().toString()方法生成。
第三个为time,代表key的过期时间
第四个为time的单位
setIfAbsent 相当于NX,设置过期时间相当于EX
也可用Jedis实现:
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>2.9.0version>
dependency>
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
@Autowired
private Jedis jedis;
public boolean tryLock(String lockKey, String clientId, long seconds) {
String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
/**
* 与 tryLock 相对应,用作释放锁
*
* @param lockKey
* @param clientId
* @return
*/
public boolean releaseLock(String lockKey, String clientId) {
//这里使用Lua脚本的方式,尽量保证原子性。
return jedis.eval(RELEASE_LOCK_SCRIPT,Collections.singletonList(lockKey),Collections.singletonList(clientId)).equals(1L);
}
对参数进行介绍:
第一个为key,我们使用key来当锁,因为key是唯一的。我们传的是lockKey
第二个为value,通过给value赋值为clientId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。clientId可以使用UUID.randomUUID().toString()方法生成。
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
第五个为time,与第四个参数相呼应,代表key的过期时间。
可以看到,我们的加锁就一行代码,保证了原子性,总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。
@Component
@Aspect
@Slf4j
public class NoRepeatSubmitAspect {
@Pointcut("@annotation(com.xxl.sso.base.annotation.RepeatSubmit)")
public void repeatSubmit(){}
@Autowired
private HttpServletRequest request;
@Autowired
private RedisLock redisLock;
@Around("repeatSubmit()")
public ReturnT around(ProceedingJoinPoint joinPoint) {
log.info("校验重复提交");
//用户的身份标识,这里用cookie只是方便做测试
String userId = CookieUtil.getValue(request, Conf.SSO_SESSIONID).split("_")[0];
String key = getKey(userId, request.getServletPath());
String clientId = getClientId();
boolean isSuccess = redisLock.tryLock(key, clientId, 3);
// 如果缓存中有这个url视为重复提交
if (isSuccess) {
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
log.error(e.getMessage());
}
//finall{
//redisLock.releaseLock(key, clientId)
//}
return ReturnT.SUCCESS;
} else {
log.error("重复提交");
return ReturnT.FAIL;
}
}
private String getKey(String token, String path) {
return token + path;
}
private String getClientId() {
return UUID.randomUUID().toString();
}
}
//测试重复提交
@GetMapping("/test-get")
//添加RepeatSubmit注解
@RepeatSubmit
public ReturnT repeatTest(){
return ReturnT.SUCCESS;
}