一、什么是幂等
幂等是一个数学与计算机科学概念。
- 在数学中,幂等用函数表达式就是:
f(x) = f(f(x))
。比如求绝对值的函数,就是幂等的,abs(x) = abs(abs(x))
。 - 计算机科学中,幂等表示一次和多次请求某一个资源应该具有同样的副作用,或者说,多次请求所产生的影响与一次请求执行的影响效果相同。
二、为什么需要幂等
举个例子:
我们开发一个转账功能,假设我们调用下游接口超时了。一般情况下,超时可能是网络传输丢包的问题,也可能是请求时没送到,还有可能是请求到了,返回结果却丢了。这时候我们是否可以重试呢?如果重试的话,是否会多转了一笔钱呢?
当前互联网的系统几乎都是解耦隔离后,会存在各个不同系统的相互远程调用。调用远程服务会有三个状态:成功,失败,或者超时。前两者都是明确的状态,而超时则是未知状态。我们转账超时的时候,如果下游转账系统做好幂等控制,我们发起重试,那即可以保证转账正常进行,又可以保证不会多转一笔。
其实除了转账这个例子,日常开发中,还有很多很多例子需要考虑幂等。比如:
- MQ(消息中间件)消费者读取消息时,有可能会读取到重复消息。(重复消费)
- 比如提交 form 表单时,如果快速点击提交按钮,可能产生了两条一样的数据(前端重复提交)
三、接口超时了,如何处理
如果调用下游接口,或者三方接口,超时了,应该如何处理?
- 如果提供了查询接口,就先查询,之后,成功按成功处理,失败按失败处理
- 如果接口有唯一性参数,则使用相同的唯一性参数重试
下面是详细两种方案处理:
- 方案一:就是下游系统提供一个对应的查询接口。如果接口超时了,先查下对应的记录,如果查到是成功,就走成功流程,如果是失败,就按失败处理。
拿我们的转账例子来说,转账系统提供一个查询转账记录的接口,如果渠道系统调用转账系统超时时,渠道系统先去查询一下这笔记录,看下这笔转账记录成功还是失败,如果成功就走成功流程,失败再重试发起转账。
- 方案二:下游接口支持幂等,上游系统如果调用超时,发起重试即可。
四、如何设计幂等
既然这么多场景需要考虑幂等,那我们如何设计幂等呢?
幂等意味着一条请求的唯一性。不管是你哪个方案去设计幂等,都需要一个全局唯一的ID,去标记这个请求是独一无二的。
- 如果你是利用唯一索引控制幂等,那唯一索引是唯一的
- 如果你是利用数据库主键控制幂等,那主键是唯一的
- 如果你是悲观锁的方式,底层标记还是全局唯一的ID
4.1 全局的唯一性ID
全局唯一性ID,我们怎么去生成呢?你可以回想下,数据库主键Id怎么生成的呢?
是的,我们可以使用UUID
,但是UUID的缺点比较明显,它字符串占用的空间比较大,生成的ID过于随机,可读性差,而且没有递增。
我们还可以使用雪花算法(Snowflake)
生成唯一性ID。
雪花算法是一种生成分布式全局唯一ID的算法,生成的ID称为
Snowflake IDs
。这种算法由Twitter创建,并用于推文的ID。
一个Snowflake ID有64位。
- 第1位:一般最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。
- 接下来前41位是时间戳,表示了自选定的时期以来的毫秒数。
- 接下来的10位代表计算机ID,防止冲突。
- 其余12位代表每台机器上生成ID的序列号,这允许在同一毫秒内创建多个Snowflake ID。
当然,全局唯一性的ID,还可以使用百度的Uidgenerator
,或者美团的Leaf
。
4.2 幂等设计的基本流程
幂等处理的过程,说到底其实就是过滤一下已经收到的请求,当然,请求一定要有一个全局唯一的ID标记
哈。然后,怎么判断请求是否之前收到过呢?把请求储存起来,收到请求时,先查下存储记录,记录存在就返回上次的结果,不存在就处理请求。
一般的幂等处理就是这样,如下:
五、几种实现幂等方案
5.1 token 令牌
token 令牌方案一般包括两个请求阶段:
- 客户端请求申请获取token,服务端生成token返回
- 客户端带着token请求,服务端校验token
流程如下:
-
客户端发起请求,申请获取token。
-
服务端生成全局唯一的token,保存到redis中(一般会设置一个过期时间),然后返回给客户端。
-
客户端带着token,发起请求。
-
服务端去 redis 确认 token 是否存在,一般用
redis.del(token)
的方式,如果存在会删除成功,即处理业务逻辑,如果删除失败不处理业务逻辑,直接返回结果。
5.2 状态机幂等
很多业务表是有状态的,比如转账流水表,就会有0-待处理,1-处理中、2-成功、3-失败
等状态。转账流水更新的时候,都会涉及流水状态更新,即涉及状态机 (即状态变更图)。我们可以利用状态机实现幂等,一起来看下它是怎么实现的。
假设设计一个转账后的回调接口,把处理中的转账流水更新为成功状态,SQL这么写:
update transfr_flow set status=2 where order_id='10126' and status=1;
状态机是怎么实现幂等的呢?
- 第1次请求来时,order_id 是
10126
,该流水的状态是处理中,值是1
,要更新为2-成功的状态
,所以该 update 语句可以正常更新数据,sql执行结果的影响行数是1,流水状态最后变成了2。 - 第2请求也过来了,它的订单号号还是
10126
,因为该流水状态已经2-成功的状态
了,所以更新结果是0,不会再处理业务逻辑,接口直接返回成功。
5.3 防重表
mysql 建立一个以唯一流水号为主键或唯一索引的表,如果插入防重表冲突即直接返回成功,如果插入成功,即去处理请求。
redis 的话,根据传入参数算 hash 值,用哈希值 setnx ,如果设置失败返回 0,直接返回成功,如果设置成功返回 1,即去处理请求。
5.4 乐观锁
什么是乐观锁?
乐观锁在操作数据时,认为别人不会同时在修改数据,因此乐观锁不会上锁。只是在执行更新的时候判断一下,在此期间别人是否修改了数据。
怎样实现乐观锁呢?
就是给表的加多一列
version
版本号,每次更新记录version
都升级一下(version=version+1
)。具体流程就是先查出当前的版本号version
,然后去更新修改数据时,确认下是不是刚刚查出的版本号,如果是才执行更新
比如,我们更新前,先查下数据,查出的版本号是version =1
select order_id,version from order where order_id='666';
然后使用version =1
和订单Id
一起作为条件,再去更新
update order set version = version +1,status='P' where order_id='666' and version =1
最后更新成功,才可以处理业务逻辑,如果更新失败,默认为重复请求,直接返回。
「如果这篇文章对你有用,请随意打赏」
如果这篇文章对你有用,请随意打赏
使用微信扫描二维码完成支付