幂等

首先幂等的范围不单只接口,消息消费也是需要业务幂等的,比如订单例子,订单服务收到退款消息消费时。其次接口幂等,service层的职责根据作者划分为业务编排,按此划分,业务校验也可能会放在这层,比如银行转账例子,校验B账户是否被冻结,A账户资金是否足够等校验,当校验不通过,都不需要走下层逻辑,直接返回或报错,这个也是业务幂等的体现。最后幂等难点其实不是在哪里做,而是怎样做,包括复用性、复杂性平衡等。

接口幂等性

为了防止上述情况的发生,我们需要提供一个防护措施,对于同一笔支付信息如果我其中某一次处理成功了,我虽然又接收到了消息,但是这时我不处理了,即保证接口的 幂等性
维基百科上的定义:
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
  • *在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。**幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的,更复杂的操作幂等保证是利用唯一交易号(流水号)实现.
任意多次执行所产生的影响均与一次执行的影响相同,这是幂等性的核心特点。其实在我们编程中主要操作就是CURD,其中读取(Retrieve)操作和删除(Delete)操作是天然幂等的,受影响的就是创建(Create)、更新(Update)。
对于业务中需要考虑幂等性的地方一般都是接口的重复请求,重复请求是指同一个请求因为某些原因被多次提交。导致这个情况会有几种场景:
  • 前端重复提交:提交订单,用户快速重复点击多次,造成后端生成多个内容重复的订单。
  • 接口超时重试:对于给第三方调用的接口,为了防止网络抖动或其他原因造成请求丢失,这样的接口一般都会设计成超时重试多次。
  • 消息重复消费:MQ消息中间件,消息重复消费。
对于一些业务场景影响比较大的,接口的幂等性是个必须要考虑的问题,例如金钱的交易方面的接口。否则一个错误的、考虑不周的接口可能会给公司带来巨额的金钱损失,那么背锅的肯定是程序员自己了。

幂等性实现方式

对于和web端交互的接口,我们可以在前端拦截一部分,例如防止表单重复提交,按钮置灰、隐藏、不可点击等方式。
但是前端做控制实际效益不是很高,懂点技术的都会模拟请求调用你的服务,所以安全的策略还是需要从后端的接口层来做。
那么后端要实现分布式接口的幂等性有哪些策略方式呢?主要可以从以下几个方面来考虑实现:

Token机制

针对前端重复连续多次点击的情况,例如用户购物提交订单,提交订单的接口就可以通过 Token 的机制实现防止重复提交。
notion image
主要流程就是:
  1. 服务端提供了发送token的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存到redis中。(微服务肯定是分布式了,如果单机就适用jvm缓存)。
  1. 然后调用业务接口请求时,把token携带过去,一般放在请求头部。
  1. 服务器判断token是否存在redis中,存在表示第一次请求,这时把redis中的token删除,继续执行业务。
  1. 如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行。

数据库去重表

往去重表里插入数据的时候,利用数据库的唯一索引特性,保证唯一的逻辑。唯一序列号可以是一个字段,例如订单的订单号,也可以是多字段的唯一性组合。例如设计如下的数据库表。
我们注意看如下这几个关键性字段,
  • serial_no:唯一序列号的值,在这里我设置的是通过注解@IdempotentKey来标识请求对象中的字段,通过对他们 MD5 加密获取对应的值。
  • source_type:业务类型,区分不同的业务,订单,支付等。
  • remark:是由标识字段的拼接成的字符串,拼接符为 “|”。
由于数据建立了 serial_nosource_typeremark 三个字段组合构成的唯一索引,所以可以通过这个来去重达到接口的幂等性,具体的代码设计如下,
因为支付宝流水号和订单号在系统中是唯一的,所以唯一序列号可由他们组合 MD5 生成,具体的生成方式如下:
生成幂等Key,如果有多个key可以通过分隔符 "|" 连接,
一切准备就绪,则可对外提供幂等性校验的接口方法,接口方法为:
当然这个接口的方法具体在项目中合理的使用就看项目要求了,可以通过@Autowire注解注入到需要使用的地方,但是缺点就是每个地方都需要调用。我个人推荐的是自定义一个注解,在需要幂等性保证的接口上加上该注解,然后通过拦截器方法拦截使用。这样简单便不会造成代码侵入和污染。
另外,使用数据库防重表的方式它有个严重的缺点,那就是系统容错性不高,如果幂等表所在的数据库连接异常或所在的服务器异常,则会导致整个系统幂等性校验出问题。如果做数据库备份来防止这种情况,又需要额外忙碌一通了啊。

Redis实现

上面介绍过防重表的设计方式和伪代码,也说过它的一个很明显的缺点。所以我们另外介绍一个Redis的实现方式。
Redis实现的方式就是将唯一序列号作为Key,唯一序列号的生成方式和上面介绍的防重表的一样,value可以是你想填的任何信息。唯一序列号也可以是一个字段,例如订单的订单号,也可以是多字段的唯一性组合。当然这里需要设置一个 key 的过期时间,否则 Redis 中会存在过多的 key。具体校验流程如下图所示,实现代码也很简单这里就不写了。
notion image
由于企业如果考虑在项目中使用 Redis,因为大部分会拿它作为缓存来使用,那么一般都会是集群的方式出现,至少肯定也会部署两台Redis服务器。所以我们使用Redis来实现接口的幂等性是最适合不过的了。

状态机

对于很多业务是有一个业务流转状态的,每个状态都有前置状态和后置状态,以及最后的结束状态。例如流程的待审批,审批中,驳回,重新发起,审批通过,审批拒绝。订单的待提交,待支付,已支付,取消。
以订单为例,已支付的状态的前置状态只能是待支付,而取消状态的前置状态只能是待支付,通过这种状态机的流转我们就可以控制请求的幂等。
假设当前状态是已支付,这时候如果支付接口又接收到了支付请求,则会抛异常或拒绝此次处理。

总结

通过以上的了解我们可以知道,针对不同的业务场景我们需要灵活的选择幂等性的实现方式。
例如防止类似于前端重复提交、重复下单的场景就可以通过 Token 的机制实现,而那些有状态前置和后置转换的场景则可以通过状态机的方式实现幂等性,对于那些重复消费和接口重试的场景则使用数据库唯一索引的方式实现更合理。
Loading...
目录
文章列表
王小扬博客
产品
Think
Git
软件开发
计算机网络
CI
DB
设计
缓存
Docker
Node
操作系统
Java
大前端
Nestjs
其他
PHP