接口幂等设计方案

       最近一直在思考怎么保障接口调用的幂等性,经过参考网上的一些资料结合自身的情况而有所得,现整理如下,做个备忘,有兴趣或者有同样需求的朋友希望可以借此找到适合你们的方法。

一、什么是接口幂等性

首先看下从网上扒了一段对幂等性的概念描述:

幂等性原本是数学上的概念,用在接口上就可以理解为:同一个接口,多次发出同一个请求,必须保证操作只执行一次。 调用接口发生异常并且重复尝试时,总是会造成系统所无法承受的损失,所以必须阻止这种现象的发生。

例如下面这些情况,如果没有实现接口幂等性会有很严重的后果:

1、 支付接口,重复支付会导致多次扣钱 ;

2、订单接口,同一个订单可能会多次创建。

二、为什么会产生接口幂等性问题

什么情况下,会产生接口幂等性的问题呢?下面列举了各种有可能产生接口幂等性问题的场景。

1、网络波动, 可能会引起重复请求

2、用户在操作时候可能会无意触发多次下单交易,甚至没有响应而有意触发多次交易应用

3、使用了失效或超时重试机制(Nginx重试、RPC重试或业务层重试等)

4、页面重复刷新

5、使用浏览器后退按钮重复之前的操作,导致重复提交表单

6、使用浏览器历史记录重复提交表单

8、定时任务重复执行

9、用户双击提交按钮

三、如何保证接口幂等性?

那么,如何保证接口幂等性呢?

解决办法一般分为两个方向,一个方向是客户端防止重复调用,一个是服务端进行校验。

1、客户端防止重复调用

     a、 按钮只可操作一次

          提交后把按钮置灰或loding状态,消除用户因为重复点击而产生的重复记录,但是客户端防止重复提交并不是绝对可靠的,优点是实现起来比较简单。

    b、使用Post/Redirect/Get模式

         在提交后执行页面重定向,这就是所谓的Post-Redirect—Get(PRG)模式,简单来说就是当用户提交连表单后,跳转到一个重定向的信息页面,这样就避免用户按F5刷新导致的重复提交,而且也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退导致同样重复提交的问题。

2、服务端防止重复调用

a、token机制

功能上允许重复提交,但要保证重复提交不产生副作用,比如点击n次只产生一条记录,具体实现就是进入页面时申请一个token,然后后面所有的请求都带上这个token,后端根据token来避免重复请求。改方案需要客户端先请求token接口,然后再调业务接口,调用流程比较复杂,流程图如下:

b、使用唯一索引防止新增脏数据

利用数据库唯一索引机制,当数据重复时,插入数据库会抛出异常,保证不会出现脏数据。但不适用于数据库分片的场景

c、乐观锁

如果更新已有数据,可以进行加锁更新,也可以设计表结构时使用乐观锁,通过version来做乐观锁,这样既能保证执行效率,又能保证幂等, 乐观锁的version版本在更新业务数据要自增

update table set version = version + 1 where id = #{id} and version = #{version}

示例: 当有重复请求的时候,第一个请求会获取当前商品的version版本号,得到的version为1,紧接着由于第一个请求还没更新商品的version,第二个请求获取的version依然也是1, 这时候第一个请求操作更新的时候带上version并作为条件并且自增更新,这时候商品的version就会变成2,当第二个请求去操作更新的时候明显version不一致导致更新失败。

select + insert or update or delete

该方案就是操作之前先查询一下,符合要求再插入,该方案在没有并发的系统中可以解决幂等问题,在单JVM有并发的时候可以用JVM加锁来保证幂等性,在分布式环境它是无法保证幂等性,可以使用分布式来保证。

d、分布式锁

如果是分布是系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供)。

e、状态机幂等

在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助 。

f、防重表

使用唯一主键去做防重表的唯一索引,比如使用订单号作为防重表的唯一索引,每一次请求都根据订单号向防重表中插入一条数据,插入成功说明可以处理后面的业务,当处理完业务逻辑之后删除防重表中的订单号数据,后续如果有重复请求,则会因为防重表唯一索引原因导致插入失败,直接返回操作失败,直到第一次请求返回结果,可以看出防重表作用就是加锁的功能。

g、缓冲队列

将请求都快速地接收下来后放入缓冲队列中,后续使用异步任务处理队列中的数据,过滤掉重复的请求,该解决方案优点是同步处理改成异步处理、高吞吐量,缺点则是不能及时地返回请求结果,需要后续轮询得处理结果。

总的来讲接口幂等的解决方案众多,但个人觉得实现成本比较大,不利于落地。

四、我的设计方案

1、首先写个注解UnicornIdempotent

这里group属性对接口幂等进行分组,默认为unicorn

value属性支持spel,用来计算接口请求唯一key

throwException属性设置是否需要将重复提交的异常抛出来反馈给客户端

2、再来一个Aspect对接口进行拦截

大体上流程如下:

a、计算幂等的唯一标识 key

b、尝试给这个key加锁,此处用redis实现的分布式锁

c、根据加锁的情况判断是否是重复提交

d、如果加锁失败是重复提交则判断是直接抛出异常还是返回上一次调用的缓存结果

e、如果加锁成功则为正常请求,执行具体的业务方法之后缓存调用结果到redis中

流程图如下:

3、使用

 a、在需要幂等的接口方法上增加UnicornIdempotent注解

b、配置文件

timeWindowSecond用来配置幂等支撑的时间窗口,超过时间窗口接口不幂等,可以根据自己的实际情况来设置

tips用来配置抛出异常对客户端的提示信息

4、总结

 a、该方法易于实现,但是存在一个时间窗口,在时间窗口内可以保证幂等,时间窗口外不保证幂等性,这个时间窗口期需要根据自己的实际情况来设定

b、需要接口设计时能够提炼出接口调用的唯一key,需要开发人员根据业务来定,此处如果没有指定唯一key,则默认将请求URL+请求参数+请求token进行MD5后加密获取

c、支持两种方式的处理:直接抛异常或者返回缓存结果

你可能感兴趣的:(接口幂等设计方案)