这么设计服务的幂等性就对了

232 阅读5分钟

对于任何有状态(涉及状态修改)的服务,在设计服务、编码时,都必须考虑幂等性。

注:无状态的(如查询服务)服务,也要考虑幂等性。


01
设计原则


一个具有幂等性的服务,要求无论重复请求在多么极端的情况下发生,都要表里如一,此时必须满足:
  • 对外:返回完全相同的结果
  • 对内:自身状态不再发生任何改变

另,在实现层面:
  • 对于服务方
重复请求是一个先决条件,我们认为请求中的字段完全一样才是重复请求。技术实现上,幂等设计必然要依赖DB事务来进行锁控制,此时牢记事务处理方式:一锁、二判、三更新。
  • 对于调用方
一定考虑所调用服务的幂等性,同时对幂等结果做处理。没有DB的应用在做幂等处理时需要严格依赖有DB应用的幂等服务。
02
一些例子
正例 ✅
  • 某个支付接口,对于多次重复的支付调用,能够返回完全相同的结果(结果字段完全一样),且内部状态不再改变,我们就说该接口满足幂等性。

反例 ❌
  • 某个转账接口,对于多次重复的转账,真的将用的余额转出了多次,预期应该只转出一次。
  • 某个更新接口,对于多次重复更新,首次更新和之后的更新返回的结果有差异。


03
最佳实践


case 1:以业务单据表做幂等

说明:
  • 对于幂等性的控制,一般在事务中进行,且一定要加锁处理
  • 只有确认当前操作此前未执行,才可以执行操作(第一次来
  • 一旦发现操作在此前已经完成(被幂等了),则不能继续做操作,避免重复处理。此时应该检查请求和DB数据的一致性(幂等一致性校验),原则上所有的字段都应该一致,至少要检验关键字段的一致性(如单号,金额,用户等)。
  • 无论是“第一次来”还是“被幂等了”,最后返回的结果,应该是完全相同的
  • 对于调用方需要判断幂等场景,并作相应的处理。

case 2:以业务幂等表做幂等

说明:
  • 首先,所有的操作都应该在事务中完成,并且严格按照:一锁二判三更新实施。
  • try-insert是一种处理幂等的模式,通过数据库主键约束和java异常处理完成幂等判断。
  • 其他的步骤类似case1,这里不再赘述。

  • 当然,不是所有的代码实现都和上述的情况相同,建议结合代码学习和实践。


    case 1和 case 2的本质区别是什么?case1和case2的主要区别在于业务单据的UK是否与业务幂等规则一致。直接用业务单做幂等,适合业务比较简单的情况,比如你就一个服务需要幂等。
    什么场景用case 1什么场景用case 2?为什么会有不一致的场景。我们遇到的情况是同一张流水表上承载了不同的产品,而不同产品的幂等规则不一致,如果UK全部加到通用流水表上,会对其产生侵入甚至产生互斥,而且UK多了会降低表的插入性能。其实不同场景就是把幂等服务和幂等表独立了出来。如果你的系统有多个服务都需要用幂等逻辑,那么这种方式的扩展性会好一点,便于后续的升级和修改。另外,在分库分表的情况下,case2中的幂等表其实还保存了业务单号,此时幂等表还承担了路由表的角色。


    04
    举一反三


    由同步调用的幂等性,可以引申出:

    • DB幂等键的设计(唯一索引的设计)

    • 消息发送和接收的幂等性处理

    • 定时任务的幂等性处理

    • 超时任务的幂等性处理

    • 全局幂等表的接入和使用

    • 分布式事务的幂等处理


    05
    Notes:幂等处理如何做


    可以从幂等处理对象操作类型进行分类

    • 对已有数据进行更新操作的实现逻辑的幂等。这个比较适合用一锁二判三更新。

    • 对于有新数据进行写入的操作的实现的幂等,这个是需要用唯一性约束来判断的。这种表的唯一设计可以直接用业务表的唯一性,也可以根据扩展和变化引入独立的幂等表。关于幂等表,核心就是在于扩展,尤其是对于那些业务规则在前期设计设计时候没有识别出来,后期又要增加控制逻辑或者幂等逻辑的时候,为了减少对业务表的冲击,引入额外的幂等表就有价值了。


    幂等表也存在一些弊端,就是引入额外的DB操作,以及后续的数据订正和迁移都要比直接用业务表做幂等复杂。


    06
    总结


    总之,对于幂等设计,只要记住本文的第一句话就好:

    无论调用方还是服务方,对于任何有状态(涉及状态修改)的服务,在设计服务、编码时,都必须考虑幂等性。


    更多代码设计内容:

    如何通过代码重构让你的代码赏心悦目?从0到1教你写好系分设计模式实践&技巧大盘点:帮你打开思路,灵活变通各类应用场景