1、接口调用存在的问题
现如今我们的系统大多拆分为分布式 SOA,或者微服务,一套系统中包含了多个子系统服务,而一个子系统服务往往会去调用另一个服务,而服务调用服务无非就是使用 RPC 通信或者 restful。既然是通信,那么就有可能在服务器处理完毕后返回结果的时候挂掉,这个时候用户端发现很久没有反应,那么就会多次点击按钮,这样请求有多次,那么处理数据的结果是否要统一呢?那是肯定的!尤其在支付场景。
2. 什么是接口幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...这就没有保证接口的幂等性。
3、什么情况下需要保证接口的幂等性
在增删改查 4 个操作中,尤为注意就是增加或者修改。
(1)查询操作
查询对于结果是不会有改变的,查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select 是天然的幂等操作。
(2)删除操作
删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回 0,删除的数据多条,返回结果多个,在不考虑返回结果的情况下,删除操作也是具有幂等性的)
(3)更新操作
修改在大多场景下结果一样,但是如果是增量修改是需要保证幂等性的,如下例子:
把表中 id 为 XXX 的记录的 A 字段值设置为 1,这种操作不管执行多少次都是幂等的。
把表中 id 为 XXX 的记录的 A 字段值增加 1,这种操作就不是幂等的。
(4)新增操作
增加在重复提交的场景下会出现幂等性问题,如以上的支付问题。
4、那么如何设计接口才能做到幂等呢?
常见的两种实现方案:
1、通过代码逻辑判断实现。
2、使用 token 机制实现。
下面以支付系统为例,分别对接口的幂等性进行说明与实现。
(1)通过代码逻辑判断实现接口幂等性,只能针对一些满足判断的逻辑实现,具有一定局限性。
用户购买商品的订单系统与支付系统;订单系统负责记录用户的购买记录已经订单的流转状态(orderStatus),支付系统用于付款,提供如下接口,订单系统与支付系统通过分布式网络交互。
boolean pay(int accountid, BigDecimal amount) // 用于付款,扣除用户的
这种情况下,支付系统已经扣款,但是订单系统因为网络原因,没有获取到确切的结果,因此订单系统需要重试。由上图可见,支付系统并没有做到接口的幂等性,订单系统第一次调用和第二次调用,用户分别被扣了两次钱,不符合幂等性原则(同一个订单,无论是调用了多少次,用户都只会扣款一次)。如果需要支持幂等性,付款接口需要修改为以下接口:
boolean pay(int orderId, int accountId, BigDecimal amount)
通过 orderId 来标定订单的唯一性,付款系统只要检测到订单已经支付过,则第二次调用不会扣款而会直接返回结果:
上文所阐述的支付系统,针对同一个订单保证支付的幂等性,一旦订单的支付状态确定之后,以后的操作都会返回相同的结果,对用户的扣款也只会有一次。这种接口的幂等性,简化到数据层面的操作:
update userAmount set amount = amount - 'value' ,paystatus = 'paid' where orderId= 'orderid' and paystatus = 'unpay'
其中 value 是用户要减少的订单,paystatus 代表支付状态,paid 代表已经支付,unpay 代表未支付,orderid 是订单号。
在上文中提到的订单系统,订单具有自己的状态(orderStatus),订单状态存在一定的流转。订单首先有提交(0),付款中(1),付款成功(2),付款失败(3),简化之后其流转路径如图:
当 orderStatus = 1 时,其前置状态只能是 0,也就是说将 orderStatus 由 0 -> 1 是需要幂等性的。
update Order set orderStatus = 1 where OrderId = 'orderid' and orderStatus = 0
当 orderStatus 处于 0,1 两种状态时,对订单执行 0 -> 1 的状态流转操作应该是具有幂等性的。这时候需要在执行 update 操作之前检测 orderStatus 是否已经 = 1,如果已经 = 1 则直接返回 true 即可。但是如果此时 orderStatus = 2,再进行订单状态 0 -> 1 时操作就无法成功,但是幂等性是针对同一个请求的,也就是针对同一个 request id 保持幂等。
这时候再执行
update Order set orderStatus = 1 where OrderId = 'orderid' and orderStatus = 0
接口会返回失败,系统没有产生修改,如果再发一次,request id 是相同的,对系统同样没有产生修改。
(2)使用 token 机制实现接口幂等性,通用性强的实现方法
token 机制实现步骤:
A、生成全局唯一的 token, token 放到 redis 或 jvm 内存,token 会在页面跳转时获取,存放到 pageScope 中,支付请求提交先获取 token 。
B、提交后后台校验 token,执行提交逻辑,提交成功同时删除 token,生成新的 token 更新 redis,这样当第一次提交后 token 更新了,页面再次提交携带的 token 是已删除的 token 后台验证会失败不让提交。
token 特点: 要申请,一次有效性,可以限流。
注意: redis 要用删除操作来判断 token,删除成功代表 token 校验通过,如果用 select + delete 来校验 token,存在并发问题,不建议使用。
转载于:接口的幂等性 - fy_qxl - 博客园
(SAW:Game Over!)