目录

分布式系统中接口的幂等性

分布式系统中接口的幂等性

前言

幂等与你是不是分布式高并发还有JavaEE都没有关系, 关键是你的接口提供的操作是不是幂等的。 幂等性是分布式系统设计中十分重要的概念, 具有这一性质的接口在设计时总是秉持这样的一种理念:

调用接口发生异常并且重复尝试时, 总是会造成系统所无法承受的损失, 所以必须阻止这种现象的发生。

场景

对于业务中需要考虑幂等性的地方一般都是接口的重复请求, 重复请求是指同一个请求因为某些原因被多次提交, 导致产生重复数据或数据不一致(假定程序业务代码没问题)。导致这种情况会有几种场景:

  • 前端重复提交:提交订单, 用户快速重复点击多次, 造成后端生成多个内容重复的订单。
  • 接口超时重试:对于给第三方调用的接口, 为了防止网络抖动或其他原因造成请求丢失, 这样的接口一般都会设计成超时重试多次。
  • 消息重复消费:MQ消息中间件, 消息重复消费。

单体架构下同样要避免重复请求, 但在单体架构转成微服务架构之后, 以上问题变得尤为突出。

为了解决以上问题, 就需要保证接口的幂等性, 接口的幂等性实际上就是接口可重复调用, 在调用方多次调用的情况下, 接口最终得到的结果是一致的。有些接口可以天然的实现幂等性, 比如查询接口, 对于查询来说, 你查询一次和两次, 对于系统来说, 没有任何影响, 查出的结果也是一样。

幂等的概念

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

老顾的理解应该是多次调用对系统的产生的影响是一样的, 即对资源的作用是一样的, 但是返回值允许不同。

幂等性是分布式系统设计中十分重要的概念, 具有这一性质的接口在设计时总是秉持这样的一种理念:调用接口发生异常并且重复尝试时, 总是会造成系统所无法承受的损失, 所以必须阻止这种现象的发生。

各类操作的幂等性

在编程中主要操作就是CURD, 其中读取(Retrieve)操作和删除(Delete)操作是天然幂等的, 受影响的就是创建(Create)、更新(Update)。

  • 查询操作

    在数据不变的情况下, 查询一次和查询多次, 查询结果是一样的;查询具有天然的幂等性。

  • 删除操作

    删除一次和多次删除的结果都是把数据删除, 对资源的作用都是一样的(注意可能返回结果不一样, 首次删除时, 返回成功;再次删除时, 数据不存在, 返回0);

  • 更新操作

    更新在大多场景下结果一样,但是如果是增量修改是需要保证幂等性的,如下例子:

      把表中id为XXX的记录的A字段值**置为1**,这种操作不管执行多少次都是幂等的
      把表中id为XXX的记录的A字段值**增加1**,这种操作就不是幂等的
    
  • 新增操作

    增加在重复提交的场景下会出现幂等性问题, 比如同一订单创建多次, 除非有唯一主键约束。

实现接口幂等的技术方案

全局唯一ID

适用场景:全局唯一ID是一个通用方案, 可以支持插入、更新、删除业务操作;但是这个方案看起来很美但是实现起来比较麻烦。

使用全局唯一ID, 就是根据业务的操作和内容生成一个全局ID, 在执行操作前先根据这个全局唯一ID是否存在, 来判断这个操作是否已经执行。如果不存在则把全局ID, 存储到存储系统中, 比如数据库、redis等。如果存在则表示该方法已经执行。

从工程的角度来说, 使用全局ID做幂等可以作为一个业务的基础微服务存在, 在很多的微服务中都会用到这样的服务。另外打造一个高可靠的幂等服务还需要考虑很多问题, 比如一台机器虽然把全局ID先写入了存储, 但是在写入之后挂了, 这就需要引入全局ID的超时机制。

使用, 下面的方案适用于特定的场景, 但是实现起来比较简单。

数据库去重表

适用场景:业务中具有唯一标识的插入场景中, 如新增类接口等;

比如在以上的支付场景中, 如果一个订单只会支付一次, 所以订单ID可以作为唯一标识。这时, 我们就可以建一张去重表, 并且把唯一标识作为唯一索引, 在我们实现时, 把创建支付单据、写入去重表这两个操作, 放在一个事务中, 如果重复创建, 数据库会抛出唯一约束异常, 操作就会回滚。

例如博客点赞问题, 要想防止一个人重复点赞, 可以设计一张表, 将博客id与用户id绑定建立唯一索引, 每当用户点赞时就往表中写入一条数据, 这样重复点赞的数据就无法写入。

多版本号控制

适用场景:更新场景; 多版本并发控制, 乐观锁的一种实现, 在数据更新时需要去比较持有数据的版本号, 版本号不一致的操作无法成功.

比如我们要更新商品的名字, 这时我们就可以在更新的接口中增加一个版本号, 来做幂等:

1
boolean updateGoodsName(int id,String newName,int version);

在实现时可以如下:

1
update goods set name=#{newName},version=#{version} where id=#{id} and version<${version}

状态机控制

适用场景:适合在有状态流转的情况下, 每个状态都有前置状态和后置状态, 以及最后的结束状态。

例如流程的待审批, 审批中, 驳回, 重新发起, 审批通过, 审批拒绝。订单的待提交, 待支付, 已支付, 取消。 以订单为例, 已支付的状态的前置状态只能是待支付, 而取消状态的前置状态只能是待支付, 通过这种状态机的流转我们就可以控制请求的幂等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public enum OrderStatusEnum {

    UN_SUBMIT(0, 0, "待提交"),
    UN_PADING(0, 1, "待支付"),
    PAYED(1, 2, "已支付待发货"),
    DELIVERING(2, 3, "已发货"),
    COMPLETE(3, 4, "已完成"),
    CANCEL(0, 5, "已取消"),
    ;

    //前置状态
    private int preStatus;

    //当前状态值
    private int status;

    //状态描述
    private String desc;

    OrderStatusEnum(int preStatus, int status, String desc) {
        this.preStatus = preStatus;
        this.status = status;
        this.desc = desc;
    }

    //...
}

假设当前状态是已支付, 这时候如果支付接口又接收到了支付请求, 则会抛异常或拒绝此次处理。在做状态机更新时, 我们就这可以这样控制, 使其只能按指定方向流转:

1
update `order` set status=#{status} where id=#{id} and status<#{status}

token机制

适用场景:防止重复提交等。

注意:redis要用删除操作来判断token, 删除成功代表token校验通过, 如果用select+delete来校验token, 存在并发问题, 不建议使用

注意:Token防重复提交, 只需要网关这层控制即可;Token的处理机制, 还需要缓存调用的处理结果, 以判断是否需要放行后续的重试请求;

主要流程就是:

  1. 服务端提供了发送token的接口。我们在分析业务的时候, 哪些业务是存在幂等问题的, 就必须在执行业务前, 先去获取token, 服务器会先把token保存到redis中。(集群环境用redis, 单机就用jvm缓存或redis)。
  2. 然后调用业务接口请求时, 把token携带过去, 一般放在请求头部。
  3. 服务器判断token是否存在redis中, 存在表示第一次请求, 可以继续执行业务, 执行业务完成后, 最后需要把redis中的token删除。
  4. 如果判断token不存在redis中, 就表示是重复操作, 直接返回重复标记给client, 这样就保证了业务代码, 不被重复执行。(意味着重复执行也只会申请一次token,而不会每次重复执行都去申请一个token)

以电商平台为例子, 电商平台上的订单id就是最适合的token。

对外提供接口的api如何保证幂等

如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源, seq序列号 source+seq在数据库里面做唯一索引, 防止多次付款, (并发时, 只能处理一个请求)

重点:
对外提供接口为了支持幂等调用, 接口有两个字段必须传, 一个是来源source, 一个是来源方序列号seq, 这个两个字段在提供方系统里面做联合唯一索引, 这样当第三方调用时, 先在本方系统里面查询一下, 是否已经处理过, 返回相应处理结果;没有处理过, 进行相应处理, 返回结果。注意, 为了幂等友好, 一定要先查询一下, 是否处理过该笔业务, 不查询直接插入业务系统, 会报错, 但实际已经处理了。

测试方案

通过下面的方法可以初步验证接口幂等性的健壮性:

  1. 同一个请求, 多次提交到同一台节点, 多次提交到不同的节点
  2. 同一个请求, 同时到达同一个节点, 同时到达到不同的节点
  3. 有逻辑先后顺序的消息、请求乱序的处理, 比如创建订单的请求和支付订单的请求, 不能保证第一个请求先于第二个请求到达服务器;

总结

幂等就是要使接口具备抵御重复请求对系统带来的破坏。 可以采取的方案有:

  1. UUID——通用
  2. 去重表——新增场景
  3. 多版本号控制——增量更新场景
  4. 状态机——带状态流转的更新场景
  5. token机制——防止数据重复提交等场景
  6. source + seq序列号——对外api
  7. 乐观锁、悲观锁、分布式锁——既可以控制并发, 也可以防止长流程中多个重复请求操作一个对象(关联下方声明)。

声明

本菜鸡并未参与过分布式高并发系统的开发, 该文只是学习记录, 特此声明, 轻喷。

看到网上还有不少教程列举了, 乐观锁, 悲观锁, 分布式锁之类的方案, 我想这应该是属于并发控制的范围, 不属于幂等实现方案。如果理解有误, 请大神不吝评论指正。

参考链接

https://blog.csdn.net/WANGYAN9110/article/details/70953273

https://blog.csdn.net/xichenguan/article/details/78085801

https://juejin.im/post/5ceb4c4f51882572a206d174

https://blog.csdn.net/rdhj5566/article/details/50646599

https://www.jianshu.com/p/475589f5cd7b