关于重复请求,指的是我们服务端接收到很短的时间内的多个相同内容的重复请求。而这样的重复请求如果是幂等的(每次请求的结果都相同,如查询请求),那其实对于我们没有什么影响,但如果是非幂等的(每次请求都会对关键数据造成影响,如删除关系、建立关系等),那就会轻则产生脏数据,重则导致系统错误。
因此,在当前普遍分布式服务的情况下,如何避免和解决重复请求给我们带来的数据异常成为了亟待解决的问题。而避免重复请求,最好的做法是前后端共同去做。
1. 前端或客户端在非幂等的按钮上直接做禁止提交重复请求的操作。
2. 后端在接收到请求时加锁,完成后解锁。
这篇博客主要讲的是在后端基于分布式锁的概念去出一个关于解决重复请求的通用解决方案。
二、正文
为何要使用分布式锁来解决呢?因为我们当前普遍的架构都是分布式的服务端,前端请求通过网关层转发至后端,如下图所示,因此如果只在一个单独的服务器上做限制,就无法在分布式的服务中完成应对高频次的重复请求了。
基本思路
思路基本上是对需要做防止重复请求的接口加上分布式锁,步骤如下:
- 在接收到请求后,根据方法名+参数取md5值,获取该方法及该参数的唯一标识;
- 获取标识后设置分布式锁,并且设置过期时间;
- 在请求结束后,释放分布式锁。
即可完成对当前请求的重复请求禁止。如果想做通用的解决方案,那就需要把上述步骤做出一个小功能出来,由于本人对java、spring框架比较熟悉,就拿这个来做个示例。
基于spring切面、redis的实现
想必一些熟悉spring的同学已经知道我想采用什么方式了,做通用型的,肯定要用到spring的aop特性,注解+切面+md5key+反射+redis实现,具体如下:
- 定义一个分布式锁注解,注解包含过期时间设置、忽略参数;
- 定义一个切面,切点为分布式锁注解,在切面中获取需要使用分布式锁的方法名、参数、过期时间,并且将方法名及未被忽略参数做md5取唯一标识;
- 再根据上述唯一标识设置redsis分布式锁;
- 方法结束后解锁。
代码如下:
注解
定义名称为RepeatOperationLock的注解,参数有锁过期时间及忽略属性(即不参与分布式锁标识MD5计算的属性)。
@Documented@Inherited@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})@Componentpublic @interface RepeatOperationLock { /** * 锁时长,默认500ms * @return */ long timeOut() default 500; /** * 忽略上锁参数位置,从0开始 * @return */ int[] ignoreIndex();}复制代码
切面
切点为上述注解,切面中做了以下几件事,获取方法名、获取注解属性(过期时间、忽略属性)、计算方法+属性的md5值、调用外部分布式锁的方法。
@Aspect@Slf4j@Componentpublic class LockAspect { @Autowired RepeatLockService repeatLockService; @Pointcut("@annotation(com.ls.javabase.aspect.annotation.RepeatOperationLock)") public void serviceAspect() { } @Before("serviceAspect()") public void setLock(JoinPoint point) { log.info("防止方法重复调用接口锁,上锁,point:{}