13.幂等设计
目录
触发幂等的场景
- 用户重复提交
- 外部恶意请求
- 接口超时重复调用
- MQ重复消费
- 定时任务重复执行
接口超时
如果我们调用下游接口超时:
- 方案一:下游服务提供一个对应的查询接口。如果接口超时了,先查询一下对应的记录,如果查询到是成功,就走成功流程,如果失败就按照失败后流程处理
- 方案二:下游接口支持幂等,上游服务如果调用超时,直接重试即可
幂等设计
方式一:select+insert+主键/唯一索引冲突
日常开发中,为了实现交易接口幂等,我是这样实现的:
- 交易请求过来,我会先根据请求的唯一流水号
bizSeq
字段,先select
一下数据库的流水表 - 如果数据已经存在,就拦截是重复请求,直接返回成功;
- 如果数据不存在,就执行
insert
插入,如果insert
成功,则直接返回成功,如果insert
产生主键冲突异常,则捕获异常,接着直接返回成功。
方式二:直接insert + 主键/唯一索引冲突
如果重复请求的概率比较低的话,我们可以直接插入请求,利用主键/唯一索引冲突,去判断是重复请求。
方式三: 状态机幂等
很多业务表,都是有状态的,比如转账流水表,就会有0-待处理,1-处理中、2-成功、3-失败状态
。转账流水更新的时候,都会涉及流水状态更新,即涉及状态机 (即状态变更图)。我们可以利用状态机实现幂等,一起来看下它是怎么实现的。
比如转账成功后,把处理中的转账流水更新为成功状态,SQL这么写:
update transfr_flow set status=2 where biz_seq=‘666’ and status=1;
简要流程图如下:
- 第1次请求来时,bizSeq流水号是
666
,该流水的状态是处理中,值是1
,要更新为2-成功的状态
,所以该update语句可以正常更新数据,sql执行结果的影响行数是1,流水状态最后变成了2。 - 第2请求也过来了,如果它的流水号还是
666
,因为该流水状态已经2-成功的状态
了,所以更新结果是0,不会再处理业务逻辑,接口直接返回。
方式四: 抽取防重表
适用于MQ消息消费
上面基本上都是建立在业务流水表上bizSeq
的唯一性上。很多时候,我们业务表唯一流水号希望后端系统生成,又或者我们希望防重功能与业务表分隔开来,这时候我们可以单独搞个防重表。当然防重表也是利用主键/索引的唯一性,如果插入防重表冲突即直接返回成功,如果插入成功,即去处理请求。
方式五: token令牌
适用于防重复提交
token 令牌方案一般包括两个请求阶段:
- 客户端请求申请获取token,服务端生成token返回
- 客户端带着token请求,服务端校验token
- 客户端发起请求,申请获取token。
- 服务端生成全局唯一的token,保存到redis中(一般会设置一个过期时间),然后返回给客户端。
- 客户端带着token,发起请求。
- 服务端去redis确认token是否存在,一般用 redis.del(token)的方式,如果存在会删除成功,即处理业务逻辑,如果删除失败不处理业务逻辑,直接返回结果。
方式六:悲观锁(如select for update)
begin; # 1.开始事务
select * from order where order_id='666' for update # 查询订单,判断状态,锁住这条记录
if(status !=处理中){
//非处理中状态,直接返回;
return ;
}
## 处理业务逻辑
update order set status='完成' where order_id='666' # 更新完成
commit; # 5.提交事务
- 这里面order_id需要是索引或主键哈,要锁住这条记录就好,如果不是索引或者主键,会锁表的!
- 悲观锁在同一事务操作过程中,锁住了一行数据。别的请求过来只能等待,如果当前事务耗时比较长,就很影响接口性能。所以一般不建议用悲观锁做这个事情。
方式七:乐观锁
方式八:分布式锁
- 分布式锁可以使用Redis,也可以使用ZooKeeper,不过还是Redis相对好点,因为较轻量级。
- Redis分布式锁,可以使用命令SET EX PX NX + 唯一流水号实现,分布式锁的key必须为业务的唯一标识哈
- Redis执行设置key的动作时,要设置过期时间哈,这个过期时间不能太短,太短拦截不了重复请求,也不能设置太长,会占存储空间。