赞
踩
幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。
“ 在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
幂等函数或幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
在 HTTP/1.1 中,对幂等性进行了定义。它描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),即第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用。
这里的副作用是不会对结果产生破坏或者产生不可预料的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
我们都知道,作为接口的调用方,对于接口调用的结果,一般会返回成功、失败和超时。对于成功和失败,都是明确的状态,调用方可以根据结果做相应的处理,而超时则是未知状态,由于不确定是否成功请求了,作为调用方来说,所以一般都会选择重试。而重试就会出现定义中描述的多次执行。我们转账超时的时候,如果下游转账系统做好幂等控制,我们发起重试,那即可以保证转账正常进行,又可以保证不会多转一笔。
可以从下面这个例子中加深一下理解:
要保证不会多件一次库存,一般有两种做法:
开发中,还有很多很多例子需要考虑幂等。比如:
在接口调用时一般情况下都能正常返回信息不会重复提交,不过在遇见以下情况时可以就会出现问题,如:
幂等性是为了简化客户端逻辑处理,能放置重复提交等操作,但却增加了服务端的逻辑复杂性和成本,其主要是:
所以在使用时候需要考虑是否引入幂等性的必要性,根据实际业务场景具体分析,除了业务上的特殊要求外,一般情况下不需要引入的接口幂等性。
两种方案处理:
拿转账例子来说,转账系统提供一个查询转账记录的接口,如果渠道系统调用转账系统超时时,渠道系统先去查询一下这笔记录,看下这笔转账记录成功还是失败,如果成功就走成功流程,失败再重试发起转账。
幂等意味着一条请求的唯一性。不管是你哪个方案去设计幂等,都需要一个全局唯一的ID,去标记这个请求是独一无二的。
我们还可以使用雪花算法(Snowflake)
生成唯一性ID。
雪花算法是一种生成分布式全局唯一ID的算法,生成的ID称为
Snowflake IDs
。这种算法由Twitter创建,并用于推文的ID。
一个Snowflake ID有64位。
还可以使用百度的Uidgenerator
,或者美团的Leaf
。
幂等处理的过程,说到底其实就是过滤一下已经收到的请求,当然,请求一定要有一个全局唯一的ID标记
。然后,怎么判断请求是否之前收到过呢?把请求储存起来,收到请求时,先查下存储记录,记录存在就返回上次的结果,不存在就处理请求。
1、数据库唯一主键如何实现幂等性
数据库唯一主键的实现主要是利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条带该唯一主键的记录。
使用数据库唯一主键完成幂等性时需要注意的是,该主键一般来说并不是使用数据库中自增主键,而是使用分布式 ID 充当主键,这样才能能保证在分布式环境下 ID 的全局唯一性。
select+insert+主键/唯一索引冲突
交易请求过来,我会先根据请求的唯一流水号 ,先select
一下数据库的流水表
insert
插入,如果insert
成功,则直接返回成功,如果insert
产生主键冲突异常,则捕获异常,接着直接返回成功。注意:核心高并发流程不要用这种方法,并发不高可以使用。
直接insert + 主键/唯一索引冲突
如果重复请求的概率比较低的话,我们可以直接插入请求,利用主键/唯一索引冲突,去判断是重复请求。
2、数据库悲观锁
假设先查出订单,如果查到的是处理中状态,就处理完业务,再然后更新订单状态为完成。如果查到订单,并且是不是处理中的状态,则直接返回。
整体的伪代码如下:
begin; # 1.开始事务
select * from order where order_id='666' # 查询订单,判断状态
if(status !=处理中){
//非处理中状态,直接返回;
return ;
}
## 处理业务逻辑
update order set status='完成' where order_id='666' # 更新完成
commit; # 5.提交事务
这种场景是非原子操作的,在高并发环境下,可能会造成一个业务被执行两次的问题:
当一个请求A在执行中时,而另一个请求B也开始状态判断的操作。因为请求A还未来得及更改状态,所以请求B也能执行成功,这就导致一个业务被执行了两次。
可以使用数据库悲观锁(select ...for update
)解决这个问题。
begin;
select * from order where order_id='666' for update # 查询订单,判断状态,锁住这条记录
if(status !=处理中){
//非处理中状态,直接返回;
return ;
}
## 处理业务逻辑
update order set status='完成' where order_id='666' # 更新完成
commit;
悲观锁在同一事务操作过程中,锁住了一行数据。别的请求过来只能等待,如果当前事务耗时比较长,就很影响接口性能。所以一般不建议用悲观锁做这个事情。
3、数据库乐观锁
数据库乐观锁方案一般只能适用于执行更新操作的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。
这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值。
比如,我们更新前,先查下数据,查出的版本号是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
最后更新成功,才可以处理业务逻辑,如果更新失败,默认为重复请求,直接返回。
注意:乐观锁的更新操作,最好用主键或者唯一索引来更新,这样是行锁,否则更新时会锁表。
4、token 机制实现
通过 token 机制实现接口的幂等性,这是一种比较通用性的实现方法。
针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用 Token
的机制实现防止重复提交。
具体流程步骤:
注意:
Lua
脚本来保证原子性。参考代码:https://github.com/zysspace/re_submint.git
5、状态机幂等
在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机
如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。
注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助。
比如转账成功后,把处理中的转账流水更新为成功状态,SQL这么写:
update transfr_flow set status=2 where biz_seq=‘666’ and status=1;
状态机是怎么实现幂等的呢?
666
,该流水的状态是处理中,值是 1
,要更新为2-成功的状态
,所以该update语句可以正常更新数据,sql执行结果的影响行数是1,流水状态最后变成了2。666
,因为该流水状态已经2-成功的状态
了,所以更新结果是0,不会再处理业务逻辑,接口直接返回成功。6、使用防重表
数据库唯一主键的实现主要是利用数据库中主键唯一约束的特性,一般来说唯一主键比较适用于“插入”时的幂等性,其能保证一张表中只能存在一条带该唯一主键的记录。
使用数据库唯一主键完成幂等性时需要注意的是,该主键一般来说并不是使用数据库中自增主键,而是使用分布式 ID 充当主键,这样才能能保证在分布式环境下 ID 的全局唯一性。
往去重表里插入数据的时候,利用数据库的唯一索引特性,保证唯一的逻辑。唯一序列号可以是一个字段,也可以是多字段的唯一性组合。
具体流程步骤:
这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
另外,使用数据库防重表的方式它有个严重的缺点,那就是系统容错性不高,如果幂等表所在的数据库连接异常或所在的服务器异常,则会导致整个系统幂等性校验出问题。
7、分布式锁
分布式锁实现幂等性的逻辑就是,请求过来时,先去尝试获得分布式锁,如果获得成功,就执行业务逻辑,反之获取失败的话,就舍弃请求直接返回成功。
分布式锁可以使用Redis,也可以使用ZooKeeper。
Redis分布式锁,是基于 SETNX 命令实现的 ,SETNX key value:将 key 的值设为 value ,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。该命令在设置成功时返回 1,设置失败时返回 0。
Redis执行设置key的动作时,要设置过期时间,这个过期时间不能太短,太短拦截不了重复请求,也不能设置太长,会占存储空间。
具体流程步骤:
8、HTTP的幂等
现在流行的 Restful 推荐的几种 HTTP 接口方法中,分别存在幂等行与不能保证幂等的方法,如下:
√
满足幂等x
不满足幂等-
可能满足也可能不满足幂等,根据实际业务逻辑有关9、对外提供接口的api如何保证幂等
如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号
source+seq在数据库里面做唯一索引,防止多次付款,(并发时,只能处理一个请求)
重点 :对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源source,一个是来源方序列号seq,这个两个字段在提供方系统里面做联合唯一索引。这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。
注意,为了幂等友好,一定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理了。
10、下游传递唯一序列号如何实现幂等性
请求序列号,其实就是每次向服务端请求时候附带一个短时间内唯一不重复的序列号,该序列号可以是一个有序 ID
,也可以是一个订单号,一般由下游生成,在调用上游服务端接口时附加该序列号和用于认证的 ID
。
当上游服务器收到请求信息后拿取该 序列号 和下游 认证ID 进行组合,形成用于操作 Redis 的 Key
,然后到 Redis 中查询是否存在对应的 Key
的键值对,根据其结果:
Key
作为 Redis 的键,以下游关键信息作为存储的值(例如下游商传递的一些业务逻辑信息),将该键值对存储到 Redis 中 ,然后再正常执行对应的业务逻辑即可。具体步骤:
ID
作为序列号,然后执行请求调用上游接口,并附带唯一序列号与请求的认证凭据ID。Key
,如果存在就抛出重复执行的异常信息,然后响应下游对应的错误信息。如果不存在就以该序列号和认证ID组合作为 Key
,以下游关键信息作为 Value
,进而存储到 Redis 中,然后正常执行接来来的业务逻辑。上面步骤中插入数据到 Redis 一定要设置过期时间。这样能保证在这个时间范围内,如果重复调用接口,则能够进行判断识别。如果不设置过期时间,很可能导致数据无限量的存入 Redis,致使 Redis 不能正常工作。
幂等性是开发当中很常见也很重要的一个需求,尤其是支付、订单等与金钱挂钩的服务,保证接口幂等性尤其重要。在实际开发中,我们需要针对不同的业务场景我们需要灵活的选择幂等性的实现方式:
Token
与 Redis
配合的“防重 Token 方案”实现更为快捷。Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。