• 防重注解@RepeatSubmit的实现原理
  • 发布于 2个月前
  • 440 热度
    0 评论
接口幂等性是指无论调用接口的次数是一次还是多次,对于同一资源的操作都只会产生一次结果。换句话说,多次重复调用相同的接口请求应该具有与单次请求相同的效果,不会导致不一致或副作用的发生。接口想要保证幂等性有很多种方案,但是这不是今天的重点,今天来介绍一下如何通过自定义注解的方式保证接口在一定时间内幂等。

场景
病历云管理系统中其实高并发的场景不是很多,没有必要每个接口都去考虑并发高的场景,比如添加住院患者的这个接口,具体的业务代码就不贴了,业务伪代码如下:

上述代码有问题吗?谁能说有问题?一般情况下是没什么问题,但是在高并发的场景下肯定是存在问题,为什么?因为有事务的隔离性,step1这个阶段对住院号的校验肯定是存在问题的,在高并发的场景下无法保证这里的校验一定准确。其实这个接口的并发并不高,在病历云管理系统中一般不会出现这种问题,那么什么时候会出现呢?

医院中大部分是内网+外网,如果由于网络的抖动,系统请求响应的时间延迟,这样会导致医护操作时会出现重复点击的情况,比如1秒中之内由于第一次点添加患者这个按钮没反应,往往护士都会重复点击,这种情况下是会出现问题。

这里我们就暂且不谈对单个接口的幂等优化了,要想一个方案全局解决这个问题,在病历云管理系统中其实只要保证这种并发不高的接口在一定时间段内保证幂等即可,比如5秒之内,这样在5秒之内护士重复点击就没事。

解决方案
在病历云管理系统中新增了一个注解:@RepeatSubmit,代码如下:

只需要将该注解标注在新增、修改、删除接口上就能保证在默认的5秒之内接口幂等。
比如新增住院患者这个接口:

那么原理是什么?其实很简单,先来说下原理,再介绍具体的实现:
1.AOP拦截增强@RepeatSubmit注解
2.获取请求的URL、IP地址、请求参数
3.将请求URL、IP地址、请求参数以一定形式转为key
4.借助Redis的setNx命令将key存入Redis,且设置失效时间
5.如果存入成功则允许访问,失败则抛出异常
6.全局异常捕获,输出指定信息给客户端
上述6个步骤中其实只有一点比较难实现的,其他的都是基本操作,就是获取这个请求参数,下面将详细介绍一下如何获取这个请求参数。

获取请求参数
对于form-data的入参只需要调用HttpServletRequest的API读取,但是对于@RequestBody标注的入参是通过IO流读取数据,且IO流只能被读取一次,如果在AOP中读取了,那么在接口层面的入参读取肯定是有问题,报错如下:

解决方案也很简单,只需要保证IO流能够多次读取即可,下面就来介绍一下方案。

这里我们可以利用装饰者模式对 HttpServletRequest 的功能进行增强,具体做法也很简单,我们重新定义一个 HttpServletRequest:

这段代码并不难,很好懂。

首先在构造 RepeatedlyRequestWrapper 的时候,就通过 IO 流将数据读取出来并存入到一个 byte 数组中,然后重写 getReader 和 getInputStream 方法,在这两个读取 IO 流的方法中,都从 byte 数组中返回 IO 流数据出来,这样就实现了反复读取了。

接下来我们定义一个过滤器,让这个装饰后的 Request 生效:

判断一下,如果请求数据类型是 JSON 的话,就把 HttpServletRequest “偷梁换柱”改为 HttpRequestWrapper,然后让过滤器继续往下走。这样就可以配置后就可以在程序中反复读取参数了!

防重注解实现
解决了参数读取的问题,下面就可以轻松实现这个防重注解了,首先定义注解com.code.ape.codeape.common.security.annotation.RepeatSubmit:

接下来直接用AOP实现,com.code.ape.codeape.common.security.component.CodeapeRepeatSubmitAspect代码如下:

逻辑很简单,上述已经介绍过完整的流程,这里需要注意的是参数的读取,代码如下:

其实就是将request判断下是否是经过过滤器封装后的HttpRequestWrapper对象,如果是的话则是@RequestBody入参,直接从IO流中读取。

总结
本节内容介绍了防重注解@RepeatSubmit的实现原理,后续开发中只需要在非查询接口中添加这个注解就能保证在一定时间内防止重复提交。
用户评论