侧边栏壁纸
博主头像
laasc

Coding changes the world

  • 累计撰写 7 篇文章
  • 累计创建 9 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

如何保证对外接口的幂等性问题讨论

laasc
2022-09-01 / 0 评论 / 1 点赞 / 624 阅读 / 2,164 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-09-01,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

前言

接口幂等性问题,对于咱们开发人员来说,是一个无关语言的公共性问题。

不知道你有没有遇到过这种场景

1.当我们有时我们在填写某些form表单提交时,提交按钮不小心快速点了两次,导致表单被重复提交,表中竟然产生了两条重复的数据,只是id不一样。

2.我们在项目中为了解决接口超时问题,会引入了重试机制。第一次请求接口超时了,请求方没能及时获取返回结果(但这个请求有可能已经成功了),为了避免返回错误的结果(这种情况不可能直接返回失败是吧),于是会对该请求重试几次,这样也会产生重复的数据。

3.MQ消费者在读取消息时,有时候会读取到重复消息进行消费(至于什么原因这里先不说),如果没有处理好,也会产生重复的数据。

这些都是幂等性问题

接口幂等性是指用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

副作用:可以认为多次请求操作,每一次对数据状态都会产生影响 。
注意这里并没有要求接口返回结果是一致的。

例如:update order set moeny = 100 where orderId = 2029282312
该操作无论执行多少次,对数据的影响都是一致的,不变的。

接口如果不做幂等性处理会怎么样?

支付场景中:用户购买商品后,发起支付操作,支付系统处理支付成功后,由于网络原因没有及时返回给用户结果,其实这个时候订单已经扣过款,相应的支付流水也都已经生成,这个时候用户又点击支付操作,此时会进行第二次扣款,扣款成功后返回给用户。用户去查看支付订单和流水会发现自己支付两次,完蛋了,要被用户投诉了,这就是接口没有进行幂等处理造成的。

类似的场景还有很多,那么我们要如何保证接口幂等性?

可以思考下,第一种用户提交表单的场景,既然是用户重复提交导致的,那我们是不是可以不让用户重复提交。

方案一:前端控制

在前端做拦截,比如按钮点击一次之后就置灰或者隐藏。但是往往前端并不可靠,还是得后端处理才更放心。

方案二:防重的标识符 (Token令牌)实现

用户在提交表单之前调用后台接口获取 token 并存入 redis,提交表单的时候携带token一起请求,后端需要用这个token到Redis中进行键值内容校验,如果Key存在且Value匹配就执行删除命令,然后执行后面的业务逻辑。如果不存在对应的Key或者Value不匹配就返回执行错误的信息。
b58115a0b4778df2d129d677bc4402e

这里为什么不先判断 redis 是否存在这个 token 再删除,是因为要保证操作的原子性,极端情况下,第一个请求查询到 redis 中存在这个 token,还没来得及删除,第二个请求进来,也查询到 redis 中存在这个 token,那么还是会造成重复提交的问题。

方案三:唯一索引

这种方案就比较简单了,使用唯一索引可以避免脏数据的添加,当插入重复数据时数据库会抛异常,保证了数据的唯一性。唯一索引可以支持插入、更新、删除业务操作。

方案四:悲观锁

这里所说的悲观锁是基于数据库层面的,在获取数据时进行加锁,当同时有多个重复请求时,其他请求都无法进行操作。悲观锁只适用于更新操作。

// 例如
select name from student where id=1 for update;
注意:id 字段一定要是主键或者唯一索引,不然会锁住整张表,这是会死人的。悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用。

在请求量比较大的情况下,使用悲观锁明显不合适,这时候就到乐观锁上场了。

方案五:乐观锁

可以通过版本号实现,为表增加一个 version 字段,当数据需要更新时,先去数据库里获取此时的version版本号。

select version from student where id=1
更新数据时首先要对比版本号,如果不相等说明已经有其他的请求去更新数据了,提示更新失败。

update student set count=count+1,version=version+1 where version=#{version}
还有一种是通过状态机实现的,其实也是乐观锁的原理。这种方法适合在有状态流转的情况下,比如订单的创建和付款,订单的创建肯定是在付款之前,这时我们可以通过在设计状态字段时,使用 int 类型,并且通过值类型的大小来实现幂等性。

update student set status=#{status} where id=1 and status<#{status}
同样,乐观锁也只适用于更新操作。

方案六:分布式锁

有时候我们的业务不仅仅是操作数据库,也可能是发送短信、消息等等,那数据库层面的锁就不适合了。这种情况下就要考虑代码层面的锁了,而 java 的自带的锁在分布式集群部署的场景下并不适用,那么就可以采用分布式锁来实现(Redis 或 Zookeeper)。

拿 Redis 分布式锁举例,比如一个订单发起支付请求,支付系统会去 Redis 缓存中查询是否存在该订单号的 Key,如果不存在,则以 Key 为订单号向 Redis 插入。查询订单是否已经支付,如果没有则进行支付,支付完成后删除该订单号的Key。通过 Redis 做到了分布式锁,只有这次订单支付请求完成,下次请求才能进来。当然这里需要设置一个Key 的过期时间,在发生异常的时候还要注意删除 Redis 的 Key。

总结

接口的幂等性是一个很常见的问题,需要根据具体业务场景的不同,选择合适的解决方案。

1

评论区