最近遇到个问题是在网络延迟的时候,用户多次点击,最后这几次请求都发送到了服务器访问相关的接口,最后执行插入。
既然知道了原因,该如何解决。当时我的第一想法就是用注解 + AOP。通过在自定义注解里定义一些相关的字段,比如过期时间即该时间内同一用户不能重复提交请求。然后把注解按需加在接口上,最后在拦截器里判断接口上是否有该接口,如果存在则拦截。
解决了这个问题那还需要解决另一个问题,就是怎么判断当前用户限定时间内访问了当前接口。其实这个也简单,可以使用Redis来做,用户名 + 接口 + 参数啥的作为唯一键,然后这个键的过期时间设置为注解里过期字段的值。设置一个过期时间可以让键过期自动释放,不然如果线程突然歇逼,该接口就一直不能访问。
这样还需要注意的一个问题是,如果你先去Redis获取这个键,然后判断这个键不存在则设置键;存在则说明还没到访问时间,返回提示。这个思路是没错的,但这样如果获取和设置分成两个操作,就不满足原子性了,那么在多线程下是会出错的。所以这样需要把俩操作变成一个原子操作。
1.导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.15.0</version>
</dependency>
2.使用注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoRepeat {
/**
* 锁名称
*/
String lockName() default "no-repeat-default-lock-";
/**
* 参数key,支持Spel
*/
String key() default "";
/**
* 获取锁的最长时间,单位默认为秒
*/
long waitTime() default 5;
/**
* 租赁时间,单位默认为秒
* 加锁的时间。超过这个时间后锁便自动解开了
*/
3.编写切面
@Slf4j
@Aspect
@Component
public class SystemAspect {
@Resource private RedissonClient redissonClient;
/**
* SPEL表达式解析器
*/
private final SpelExpressionParser parser = new SpelExpressionParser();
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); @Around(value = "@annotation(NoRepeat)")
public Object doNoRepeatAround(ProceedingJoinPoint pjp) throws InterruptedException
{
NoRepeat noRepeat = CsUtil.getJoinPointAnnotation(pjp, NoRepeat.class);
String key = parseKey(pjp, noRepeat);
RLock fairLock = redissonClient.getFairLock(noRepeat.lockName() + key);
boolean res = fairLock.tryLock(noRepeat.waitTime(), noRepeat.leaseTime(), TimeUnit.SECONDS);
if (res) {
log.info("拿到锁");
try {
return pjp.proceed();
} catch (Throwable e) {
log.error(e.getMessage(), e);
throw new ServiceException(e.getMessage());
} finally {
fairLock.unlock(); log.info("释放锁操作"); }
} else {
log.error("重复的请求");
return new Response<Boolean>(999, "重复的请求");
}
}
private String parseKey(ProceedingJoinPoint pjp, NoRepeat noRepeat) {
Object[] args = pjp.getArgs();
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
String[] params = discoverer.getParameterNames(method);
EvaluationContext context = new StandardEvaluationContext();
for (int len = 0; len < Objects.requireNonNull(params).length; len++) {
context.setVariable(params[len], args[len]); }
String keySpel = noRepeat.key(); Expression keyExpression = parser.parseExpression(keySpel);
return keyExpression.getValue(context, String.class);
}
}
- 使用方式
@RestController
public class TestController {
@ApiOperation("身份绑定(注册)接口")
@NoRepeat(key = "#user.name")
@PostMapping("/v1/test/noRepeat")
public Response<User> doSome(@RequestBody User user) {
return new Response<>();
}
@Data
static class User {
private String name;
}
}