重复请求怎么办?
在Web开发中,重复请求的问题是不能避免的。
具体问题要具体分析,我们先来看一下,重复请求有几种。
- 首先有一个接口,假设有一个人手速很快,服务器响应都跟不上他的手速,请求过来后,这个人没有休息,以非人的速度再次点击,服务器在一瞬间收到了很多同样的请求,一时间服务器不知所措...
- 假如有一个发短信验证码的接口,又有一个手速和之前那个不相上下的人,而这个接口又没有拦截或者只有前端拦截,而这个人绕过了你的拦截,然后短信就嗖嗖嗖的发出去,钱也哗哗哗的流出去。
- 还是有一个接口,只不过这个接口同时有很多人请求,按理说每个用户的调用是相互隔离的,不会有问题,可是如果他们操作同一张表,即在数据操作上有交集,这时数据就会变的乱七八糟。
其实一提到重复请求,大家肯定都能想到Redis,一般都用Redis来作为分布式锁,来处理重复请求等等。
大概的步骤如下
- 一进到接口里,先查一下
Redis里有没有指定key的数据 - 如果有那就直接返回提示信息,没有的话,就在
Redis放个数据,然后执行具体的业务操作 - 执行完毕,删除
Redis中这个key的数据。
这个key一般都是根据业务来设定的。
升级一下
当Redis遇到AOP、Annotation会发生什么?
其实上面那样的方式我也用了很久,也没有觉得哪里不对。如果一个接口要用分布式锁,我就按照上面的写一套,甚至为了偷懒,我在Idea自定义了一个Live Template。
程序员就应该有讨厌重复劳动的高贵品质,重复劳动就是浪费生命。
首先
在Java中,注解最好用了,需要加锁,加一个注解就好了,多完美。
所以先定义一个注解。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Lock {
/**
* @return 锁的名称
*/
String name() default "";
/**
* @return 锁定时间,默认3秒
*/
int value() default 3;
/**
* @return 是否强制模式
* true 一次请求后,必须经过锁定时间后才可访问
* false 方法执行完后会自动删除锁,即方法执行完就可以再次访问
*/
boolean hard() default false;
/**
* @return 是否是分布式锁
* true 分布式锁 所有用户同一把锁,即key相同
* false 每个用户锁不同,防止单个用户重复提交(提交过快 , 如点击按钮太快)
*/
boolean distributed() default true;
}
想法很美好,现实还是需要自己努力。有了注解,我们要对使用了注解的方法进行处理。
AOP
其实一开始我想到的是拦截器,可是最后还是觉得AOP的环绕增强最适合。
@Slf4j
@Aspect
@Component
public class RepeatRequestAspect {
@Resource
private RedisUtil redisUtil;
/**
* 切面
*/
@Pointcut("@annotation(top.lww0511.redislock.annotation.Lock)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) {
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
String ipAddr = IPUtils.getIpAddr(request);
String servletPath = request.getServletPath();
Method method = ((MethodSignature) point.getSignature()).getMethod();
Lock lock = method.getAnnotation(Lock.class);
int lockTime = lock.value();
boolean hard = lock.hard();
boolean distributed = lock.distributed();
String lockKey = RedisKey.REQUEST_PREFIX + servletPath + (distributed ? "" : ("_" + ipAddr));
lockKey = StringUtils.isEmpty(lock.name()) ? lockKey : RedisKey.REQUEST_PREFIX + lock.name() + (distributed ? "" : ("_" + ipAddr));
log.info("RepeatRequestAspect_around_lockTime:{}, hard:{}, lockKey:{}", lockTime, hard, lockKey);
String value = redisUtil.getValue(lockKey);
if (!StringUtils.isEmpty(value)) {
Assert.isTrue(false, "操作太频繁了,请休息一会再操作!");
}
redisUtil.setValue(lockKey, lockKey, lockTime, TimeUnit.SECONDS);
Object proceed = null;
try {
proceed = point.proceed();
} catch (Throwable throwable) {
log.error("RepeatRequestAspect_around_throwable:{}", throwable);
} finally {
if (!hard) {
redisUtil.remove(lockKey);
}
}
return proceed;
}
}
没有复杂的代码,就是对所有加了Lock注解的方法加强,获取注解的值,比如锁定时间,是否强制模式,是否是分布式锁。
- 强制模式通俗讲就是,假如我设置锁定时间10秒,那么必须10秒后才能访问,就算提前执行完了也不能访问。不会自动删除作为锁的
Redis中的数据。 - 分布式锁就简单易懂了,设置为分布式锁,那么同一方法的
key是相同的,即一个方法同一时间只能一个用户访问,否则只是单纯的防御那些手速超快的强人。 - 至于注解里的
name(锁的名称),其实没什么用,就是任性...
使用方式
要配置Redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.database=0
加入依赖
<dependency>
<groupId>top.lww0511</groupId>
<artifactId>redis-lock</artifactId>
<version>1.0.2</version>
</dependency>
在方法上添加注解 Lock
@Lock(value = 10, distributed = false, hard = true)
@GetMapping(value = "/hi", name = "log")
public HttpResult hello() {
return HttpResult.success("Hello");
}
@Lock:默认的,表示分布式,锁定时间3秒,非强制模式@Lock(value = 10, distributed = false, hard = true):表示不是分布式的,锁定时间10秒,且是强制模式。
其实不是特殊需求,一般一个@Lock就够了。
效果
非分布式绑定了IP,并且是强制模式,虽然执行完了,方法还是不能访问的。
当只有一个@Lock时
没有绑定IP,且虽然锁定时间3秒,但是方法执行完毕,自动释放了。
最后
其中还有一些工具类,全局异常拦截器等等,还有META-INF/spring.factories。
详细代码可以看GitHub
地址:github.com/LerDer/redi…
这个已经发布到Maven中心仓库了,所以直接添加依赖就可以使用了。不需要自己打包发布到私服。
- 坐标
<!-- https://mvnrepository.com/artifact/top.lww0511/redis-lock -->
<dependency>
<groupId>top.lww0511</groupId>
<artifactId>redis-lock</artifactId>
<version>1.0.2</version>
</dependency>
欢迎大家关注我的公众号,共同学习,一起进步。加油🤣
搜索:南诏Blog