乐趣区

关于java:基于幂等表思想的幂等实践

一、为什么须要幂等

  • 分布式场景下,多个业务零碎间实现强统一的协定是极其艰难的。一个最简略和可实现的假如就是保障最终一致性,这要求服务端在解决一个反复的申请时须要给出雷同的回应,同时不会对长久化数据产生副作用(即屡次操作与单次操作的后果须要是业务角度统一的)。
  • 一个 API 领有幂等能力的话,调用发起方就能够很平安的进行重试。这合乎咱们广泛的假如。提供幂等能力是服务提供方必须须要做的事。
  • 领有幂等能力的话能够保障咱们的接口不会被各种异样重试或歹意申请锁冲击。

二、幂等形式

不同的场景下(常见的是【界面和后端接口交互场景】和【接口于接口交互场景】),幂等形式有很多并且各不相同,都各有一些局限性和毛病!!!

  • 能够基于【业务 key + 业务状态机 + 乐观锁】去做幂等实现(个别实用于比较简单的 update 场景)

    • 比方:更新订单状态为 finished 的场景中,先依据订单号查问订单,判断订单状态是否为finished,若不是则更新为finished
  • 能够基于【业务 key + 分布式锁 + 业务状态机】去做幂等实现

    • 比方:新增用户信息场景中,执行办法前先加分布式锁(防并发),以用户身份证号为查问条件,查问用户,如果用户不存在则进行新增。如果用户存在则幂等解决
    • 这种计划个别有个固定的流程:【一锁、二判、三执行
  • 能够基于【业务 key + 惟一索引】去做幂等实现(个别实用于新增数据场景)

    • 比方:新增用户信息场景中,以用户身份证号为惟一键,建设惟一索引,新增用户时通过捕捉惟一键抵触异样(DuplicateKeyException)进行幂等管制
  • 能够基于【Redis + token 模式】去做幂等实现(多用于界面和接口交互,接口于接口交互不太实用,该计划也是比拟常见的计划,但不在本次探讨范畴中)

    • 比方:用户提交填写好的表单信息,多次重复提交时保障仅仅真正执行一次,其余的都幂等返回雷同后果
  • 能够基于【幂等表】去做幂等实现(比拟通用的一种计划,具体效率取决于存储幂等记录的存储介质)

    • 比方:生产 MQ 音讯时,为了防止音讯反复生产,生产音讯前能够先插入一条幂等记录,而后再执行生产逻辑,生产实现后批改幂等记录的幂等状态为生产胜利!
    • 接口互调的状况相似

三、幂等设计准则

一个具备幂等性的服务,要求无论 反复申请 在如许极其的状况下产生,都要 表里如一,此时必须满足:

  • 对外:返回完全相同的后果
  • 对内:本身状态不再产生任何扭转
  • 对于服务提供方来说 :严格来说须要申请中的字段齐全一样,服务提供刚才认为是反复申请。然而在理论环境中咱们可能没有这么严格的要求,咱们个别认为只有 要害的业务参数雷同,那么他就属于反复申请,应该被幂等解决。
  • 对于服务调用方来说 :须要 做好幂等后果解决,屡次申请返回雷同后果须要正确被解决
  • 幂等设计要尽量从 简略、牢靠、高效(过多的幂等逻辑会对可用性和性能造成影响)角度登程

    • 简略:幂等流程和逻辑要尽量简略
    • 牢靠:不仅仅在失常运行的状况下要保障幂等的可靠性,在某些异样场景下也要尽量保障幂等的可靠性,否则该幂等设计的意义将大打折扣
    • 高效:幂等逻辑执行不能高耗时,针对于一些高并发的接口须要做到尽量减少幂等逻辑执行耗时
  • 通用幂等组件设计易用性和可扩展性也同样重要

四、常见幂等场景例子

  • MQ 音讯生产场景中】,因为 MQ 为了保障音讯投递胜利,可能会发动多次重试,那么消费者不便须要保障反复的音讯可能被幂等解决(比方:监听用户领取胜利音讯进行生成领取单)
  • 界面和接口交互场景中】,前端反复提交数据,后盾接口须要保障只执行一次,其余反复申请均幂等返回(比方:用户反复提交订单、反复提交录入的用户信息)
  • 接口互调的场景中】,调用方可能因为多种起因没能收到响应后果而发动重试(比方:数据同步、库存扣减等),此时被调用方须要保障反复调用幂等解决

五、幂等实际

  • 为了将幂等这个常见的通用需要尽量设计得通用化,咱们这里采纳【幂等表 + 幂等状态机】来实现,该计划能够实用于绝大部分的【界面 + 接口交互】和【接口 与 接口交互】模式
  • 如果我的项目仅仅是界面和接口交互模式,那么采纳【Redis + token】计划也是一个不错的抉择
  • 当然,软件工程中简直没有银弹,很难有一种完满实用与所有场景的计划

1、设计流程

  1. 调用放发动申请,申请达到服务提供方
  2. 获取指定的业务 key 作为惟一的幂等键,构建幂等记录(此时幂等记录 status 为解决中(processing)),而后尝试将幂等记录写入存储介质(能够是 Redis 也能够是 MySQL 或其余存储介质)
  3. 如果幂等记录写入胜利,则执行业务逻辑
  4. 业务逻辑执行结束,通过惟一键批改幂等记录的 status 为胜利(success
  5. 如果幂等记录写入失败,则阐明幂等记录已存在(该业务 key 对应的数据,之前有被执行过),须要进行如下解决:
  6. 通过幂等惟一键查问幂等记录,并且断定幂等记录的 status

    1. 如果 status 为胜利(success),则阐明上次曾经执行过该业务了,本次无需再反复执行,获取上次执行的后果(如果有需要的话)幂等返回即可
    2. 如果 status 为解决中(processing),则阐明曾经有其余线程正在解决业务数据 或者是 极其状况下利用宕机导致的异常情况。此时须要断定【申请处于解决中(processing)状态的时长 】,并且联合利用配置的【 容许的最大业务执行时长】进行判断

      • 处于 processing 状态的工夫曾经超过配置的【容许的最大业务执行时长 】,则尝试以 乐观锁 的形式从新批改幂等记录,如果批改胜利则执行业务逻辑,反之则抛出并发异样。
      • 处于 processing 状态的工夫没有超过配置的【容许的最大业务执行时长】,那么间接抛出并发申请异样

2、问题思考

对于上述的幂等实现流程中,极其状况下,有如下几点须要思考和留神的问题点

  • 极其状况下,如果插入幂等记录胜利,并且失常执行了业务流程,此时更新幂等状态为 success 时出现异常(比方存储幂等记录的存储介质宕机了),此时是否须要解决该异样,还是说抛出异常中断流程???如果抛出异样会有什么影响?如果 catch 异样会有什么影响?

    • 计划一、抛出异样:如果抛出异常中断流程,那么调用方应该感知到调用失败了,然而实际上业务流程曾经执行结束,这种状况如果调用方发动重试,那么幂等便会生效(同一个业务 code 被执行了两次)
    • 计划二、不抛出异样:如果不抛出异样,接口会继续执行,而后返回数据给调用方,如果调用方收到了返回数据,那么便不会发动重试了,不会有幂等问题。然而此时幂等记录的状态依然是解决中(processing),再指定了 业务最大执行工夫 的状况下,如果调用方【超过指定的最大执行】工夫再次发动重试,那么幂等依然生效(当然咱们能够不指定业务最大执行工夫)
    • 通过上述两种状况的比拟,咱们个别偏向第二种计划,本人解决掉异样,并且做一层 【兜底策略】(比方告警或记录该条幂等数据信息等,后续能够转人工核查该数据),这种计划更加稳固和实用
  • 如果业务逻辑执行失败,那么是否应该删除之前创立的幂等记录?

    • 依照严格的幂等含意来说,咱们应该保留这条幂等记录,并且将幂等记录的状态批改为Exception 或 failed,后续有重试申请进来时执行返回 failed 给调用方即可(保障屡次调用失去的后果雷同)。

      • 然而在实在环境中业务执行异样有可能是数据校验失败、接口里调用内部零碎失败(比方内部零碎正在发版(没有做优雅公布)等)
      • 针对于这些状况可能调用方批改数据后进行重试或过肯定工夫后进行重试,那么此时最好有肯定的自愈能力,而不是每次这种数据都转人工解决(一些场景中会加大人力老本,比方我之前波及到的某个零碎,常常有些调用方传递的业务参数有问题或接口里调用内部零碎失败的状况)
      • 当然这两种策略须要依据具体的状况来抉择,没有谁好谁坏之分。
  • 是否须要设定【最大的解决工夫】,比方咱们冀望接口最大解决工夫为 1 小时(也就是说幂等记录解决 processing 状态的工夫最大为 1h),如果超过这个工夫,那么认为这不是一种失常的 case,下次重试申请时应该尝试复原业务执行。

    • 这也是个具备两面性的抉择问题,须要依据理论我的项目状况衡量抉择
退出移动版