深入浅出接口幂等性的实现方式

网友投稿 897 2022-11-14

深入浅出接口幂等性的实现方式

深入浅出接口幂等性的实现方式

导读

什么是幂等性

所谓幂等性通俗的将就是一次请求和多次请求同一个资源产生相同的副作用。用数学语言表达就是​​f(x)=f(f(x))​​。维基百科的幂等性定义如下:

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的,更复杂的操作幂等保证是利用唯一交易号(流水号)实现.

为什么需要幂等性

在系统高并发的环境下,很有可能因为网络,阻塞等等问题导致客户端或者调用方并不能及时的收到服务端的反馈甚至是调用超时的问题。总之,就是请求方调用了你的服务,但是没有收到任何的信息,完全懵逼的状态。比如订单的问题,可能会遇到如下的几个问题:

创建订单时,第一次调用服务超时,再次调用是否产生两笔订单?订单创建成功去减库存时,第一次减库存超时,是否会多扣一次?订单支付时,服务端扣钱成功,但是接口反馈超时,此时再次调用支付,是否会多扣一笔呢?

作为消费者,前两种能接受,第三种情况就MMP了,哈哈哈!!!这种情况一般有如下两种解决方式

服务方提供一个查询操作是否成功的api,第一次超时之后,调用方调用查询接口,如果查到了就走成功的流程,失败了就走失败的流程。另一种就是服务方需要使用幂等的方式保证一次和多次的请求结果一致。

HTTP的幂等性

GET:只是获取资源,对资源本身没有任何副作用,天然的幂等性。HEAD:本质上和GET一样,获取头信息,主要是探活的作用,具有幂等性。OPTIONS:获取当前URL所支持的方法,因此也是具有幂等性的。DELETE:用于删除资源,有副作用,但是它应该满足幂等性,比如根据id删除某一个资源,调用方可以调用N次而不用担心引起的错误(根据业务需求而变)。PUT:用于更新资源,有副作用,但是它应该满足幂等性,比如根据id更新数据,调用多次和N次的作用是相同的(根据业务需求而变)。POST:用于添加资源,多次提交很可能产生副作用,比如订单提交,多次提交很可能产生多笔订单。

幂等性的实现方式

​​返回顶部​​

数据库去重表

在往数据库中插入数据的时候,利用数据库唯一索引特性,保证数据唯一。比如订单的流水号,也可以是多个字段的组合。实现比较简单,读者可以自己实现看看,这里不再提供demo了。

​​返回顶部​​

状态机

很多业务中多有多个状态,比如订单的状态有提交、待支付、已支付、取消、退款等等状态。后端可以根据不同的状态去保证幂等性,比如在退款的时候,一定要保证这笔订单是已支付的状态。业务中常常出现,读者可以自己实现看看,不再提供demo。

​​返回顶部​​

TOKEN机制

主要的流程步骤如下:

客户端先发送获取token的请求,服务端会生成一个全局唯一的ID保存在redis中,同时把这个ID返回给客户端。客户端调用业务请求的时候必须携带这个token,一般放在请求头上。服务端会校验这个Token,如果校验成功,则执行业务。如果校验失败,则表示重复操作,直接返回指定的结果给客户端。

通过以上的流程分析,唯一的重点就是这个全局唯一ID如何生成,在分布式服务中往往都会有一个生成全局ID的服务来保证ID的唯一性,但是工程量和实现难度比较大,UUID的数据量相对有些大,此处陈某选择的是雪花算法生成全局唯一ID,不了解雪花算法的读者下一篇文章会着重介绍。

代码实现

陈某选择的环境是SpringBoot+Redis单机环境+注解+-的方式实现,只是演示一下思想,具体的代码可以参照实现。redis如何实现,获取Token接口将全局唯一Id存入Redis(一定要设置失效时间,根据业务需求),业务请求的时候直接从redis中删除,根据delete的返回值判断,返回true表示第一次请求,返回false表示重复请求。代码如下:

@Servicepublic class TokenServiceImpl implements TokenService { @Autowired private StringRedisTemplate stringRedisTemplate; @Override public String getToken() { //获取全局唯一id long nextId = SnowflakeUtil.nextId(); //存入redis,设置10分钟失效 stringRedisTemplate.opsForValue().set(String.valueOf(nextId), UUID.randomUUID().toString(),10, TimeUnit.MINUTES); return String.valueOf(nextId); } /** * 删除记录,true表示第一次提交,false重复提交 */ @Override public Boolean checkToken(String token) { return stringRedisTemplate.delete(token); }}

注解的实现如下,标注在controller类上表示当前类上全部接口都做幂等,标注单个方法上,表示单个接口做幂等操作。

/** * @Description 幂等操作的注解 * @Author CJB * @Date 2020/3/25 10:19 */@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RepeatLimiter {}

请求头的-,用于提取请求头和校验请求头,如下:

/** * @Description 获取请求头的信息,具体校验逻辑读者自己实现 * @Author CJB * @Date 2020/3/25 11:09 */@Componentpublic class HeaderIntercept implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //获取token String token = request.getHeader(HeaderConstant.TOKEN); //校验逻辑 if (!validToken(token)) throw new TokenInvalidException("TOKEN失效"); //获取其他的参数..... RequestHeader header = RequestHeader.builder() .token(token) .build(); //放入request中 request.setAttribute(HeaderConstant.HEADER_INFO,header); return true; } /** * 校验token,逻辑自己实现 * @param token * @return */ private boolean validToken(String token){ return Boolean.TRUE; }}

保证幂等性的-,直接从redis中删除token,成功则第一次提交,不成功则重复提交。

@Componentpublic class RepeatIntercept implements HandlerInterceptor { @Autowired private TokenService tokenService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod){ //获取方法上的参数 RepeatLimiter repeatLimiter = AnnotationUtils.findAnnotation(((HandlerMethod) handler).getMethod(), RepeatLimiter.class); if (Objects.isNull(repeatLimiter)){ //获取controller类上注解 repeatLimiter=AnnotationUtils.findAnnotation(((HandlerMethod) handler).getBean().getClass(),RepeatLimiter.class); } //使用注解,需要拦截验证 if (Objects.nonNull(repeatLimiter)){ //获取全局token,表单提交的唯一id RequestHeader info = RequestContextUtils.getHeaderInfo(); //没有携带token,抛异常,这里的异常需要全局捕获 if (StringUtils.isEmpty(info.getToken())) throw new RepeatException(); //校验token Boolean flag = tokenService.checkToken(info.getToken()); //删除失败,表示 if (Boolean.FALSE.equals(flag)) //抛出重复提交的异常 throw new RepeatException(); } } return true; }}

接口幂等实现,代码如下:

@RestController@RequestMapping("/order")public class OrderController { @Autowired private OrderService orderService; /** * 下单 * @param order * @return */ @PostMapping @RepeatLimiter //幂等性保证 public CommenResult add(@RequestBody Order order){ orderService.save(order); return new CommenResult("200","下单成功"); }}

演示

发送getToken的请求获取Token

携带Token下单第一次:

第二次下单:

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:CDN到底是什么?有什么作用?
下一篇:SpringBoot 自定义注解之脱敏注解详解
相关文章

 发表评论

暂时没有评论,来抢沙发吧~