# 幂等设计

# 什么是幂等

  • 幂等性是系统服务对外一种承诺,承诺只要调用接口成功,外部多次调用对系统的影响是一致的。
  • 声明为幂等的服务会认为外部调用失败是常态,并且失败之后必然会有重试。

# 什么情况下需要幂等

  1. 业务出现异常(例如:网络异常或者服务崩溃重启)时,前端有可能重复发起超时的或者处理失败的请求。例如:用户下达委托时,由于网络问题导致超时,此时客户端很可能会重试下达,且重试下达后,服务端不会被多次执行,避免出现做超的情况。
  2. 用户下达委托时,多次点击下达按钮,可能生成多笔委托,实际期望应该只生成一笔委托。
  3. 后端处理MQ或者消息中心发过来的消息,处理失败后,要支持重试处理。避免一次处理失败,就需要人工介入的问题。 同时还需要避免被重复处理,导致业务异常。

# 幂等的优点

  • 客户端开发逻辑变得简单,处理失败后可以放心的重试,不用担心服务端重复处理的问题。

# 幂等的缺点

  • 幂等增加了服务提供者的逻辑和成本,是否有必要,需要根据具体场景具体分析

    • 增加了额外控制幂等的业务逻辑,复杂化了业务功能
    • 把并行执行的功能改为串行执行,降低了执行效率
  • 因此除了业务上的特殊要求外,尽量不提供幂等的接口。

# 业务是否需要的判断标准

  • 必须实现
    • 幂等不实现会对客户产生严重影响的
      • 例如:指令下达、委托下达,多次处理后产生多笔指令或者委托,买入或者卖出的量与客户预期的可能会差很多,对客户来说后果是很严重的。
    • 幂等实现比较简单的。
      • 例如:判断一下原状态
  • 无需实现
    • 业务处理后,会产生多笔业务或者记录,但是对客户来说影响不大的,无需实现。
      • 例如:操作日志因为不幂等多了几笔。撤单委托多了几笔,但对客户实际业务无影响。

# 幂等的空间与时间

  • 空间维度:即幂等对象的范围,是个人还是机构,是某一次交易还是某种类型的交易
  • 时间维度:即幂等的保证时间,是几秒、几分钟还是永久性的...

对于交易系统来说,幂等的空间是客户端的单笔业务请求。幂等的时间要求暂定:1秒-12小时。(小于1秒的,则要求同步串行处理。)

# 幂等要求的范围

对全系统接口进行分析,看是否有必要实现幂等。主要评价两个点:

  1. 实现幂等,增加的业务复杂度。
  2. 不做幂等带来的后果严重程度。

# 防重 VS 幂等

  • 防重:防重是指在第一次请求已经成功的情况下,人为的进行多次操作,导致不满足幂等要求的服务多次改变状态。 幂等:幂等是指在第一次请求不知道结果(比如超时)或者失败的异常情况下,发起多次请求,目的是多次确认第一次请求成功,却不会因多次请求而出现多次的状态变化。

# 保证幂等策略

幂等需要通过唯一的业务单号来保证。也就是说相同的业务单号,认为是同一笔业务。使用这个唯一的业务单号来确保,后面多次的相同的业务单号的处理逻辑和执行效果是一致的。 下面以支付为例,在不考虑并发的情况下,实现幂等很简单:①先查询一下订单是否已经支付过,②如果已经支付过,则返回支付成功;如果没有支付,进行支付流程,修改订单状态为‘已支付’。

# 防重复提交策略

上述的保证幂等方案是分成两步的,第②步依赖第①步的查询结果,无法保证原子性的。在高并发下就会出现下面的情况:第二次请求在第一次请求第②步订单状态还没有修改为‘已支付状态’的情况下到来。既然得出了这个结论,余下的问题也就变得简单:把查询和变更状态操作加锁,将并行操作改为串行操作。

# 幂等实现思路

幂等实现的思路有以下几种:

  • 业务去重 这种方法适用于在业务中有唯一标的插入场景中。比如:成交回报。成交存在唯一的成交编号,将成交编号作为唯一索引,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。

    方案优点:依懒于业务设计,无额外的性能付出。(推荐指数:※※※※※)

  • 单独去重 这种方法适用于原业务无唯一标志,客户端或服务端生成统一的唯一编号,作为业务的唯一标志。每次请求时,带上唯一编号,业务处理的事务中,同时将唯一编号插入专门的去重表。重复处理时,数据库会抛出唯一约束异常,操作就会回滚。 方案优点:不依懒业务有唯一主键。(推荐指数:※※※※)

    方案缺点:

    1. 需要增加额外的去重表。
    2. 多插入一张去重表,性能会有所损耗。
  • 多版本控制 这种方法适合在更新的场景中,我们就可以在更新的接口中增加一个版本号,来做幂等

    方案优点:实现简单,除增加一个版本字段外,无额外增加。(推荐指数:※※※※)

    方案缺点:仅适用于更新操作。

  • 状态机控制

    方案优点:性能可以保证。(推荐指数:※※※)

    方案缺点:状态必须有严格的顺序。

  • token令牌 这种方式分成两个阶段:申请token阶段和支付阶段。

    1. 在进入到提交订单页面之前,需要订单系统根据用户信息向支付系统发起一次申请token的请求,支付系统将token保存到Redis缓存中,为第二阶段支付使用。
    2. 订单系统拿着申请到的token发起支付请求,支付系统会检查Redis中是否存在该token,如果存在,表示第一次发起支付请求,删除缓存中token后开始支付逻辑处理;如果缓存中不存在,表示非法请求。 实际上这里的token是一个信物,支付系统根据token确认,你是你妈的孩子。

    方案缺点:需要系统间交互两次,流程较上述方法复杂。(推荐指数:※※※)

  • 分布式锁 这里使用的防重表可以使用分布式锁代替,比如Redis。订单发起支付请求,支付系统会去Redis缓存中查询是否存在该订单号的Key,如果不存在,则向Redis增加Key为订单号。查询订单支付已经支付,如果没有则进行支付,支付完成后删除该订单号的Key。通过Redis做到了分布式锁,只有这次订单订单支付请求完成,下次请求才能进来。相比去重表,将放并发做到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。

    方案缺点: (推荐指数:※)

    1. 需要引入分布式锁,比较麻烦。
    2. 分布式锁的性能相对来说较低。
    3. 如果业务处理失败,分布式锁无人解除,业务无法继续执行。
Last Updated: 5/24/2021, 10:37:17 AM