接口幂等性问题和常见的解决方案

在这里插入图片描述

什么是接口幂等性

  • 幂等性:就是用户对于同一操作发起的一次请求或者多次请求结果是一致的
  • 在增删改查4个操作中,尤其需要注意修改增加操作,查询不会改变数据,删除只会进行一次且多次点击产生的结果一样,修改增加在重复提交的场景下会出现接口幂等性问题。

接口幂等性问题是指多个请求只能有一个生效, 其它不生效, 比如支付, 当第一次请求如果接口不满足幂等性,可能会产生脏数据,甚至会影响业务功能,比如用户已经支付过了,因为网络等原因没有及时反馈导致用户多次点击支付按钮造成重复支付,这样肯定是不合理的。

其它场景也会产生接口幂等性的问题:

  • 定时任务重复执行。

  • 使用了失效或超时重试机制,发起的重试。

  • 第三方平台的接口,因为异常导致多次异步回调。

  • 中间件、应用服务根据自身特性,也有可能进行重试。

  • 使用浏览器后退按钮重复之前的操作,导致重复提交表单。

  • 网络波动等异常,未收到反馈后发起重复请求,页面重复刷新。

  • 用户在使用的时候无意多次点击(重复操作),或者没有响应而导致多次下单或者交易。

接口幂等性的解决方案

解决方案主要分为两个技术方向,一个是客户端防止重复调用,另一个是服务端进行校验。客户端防止重复提交并不是绝对可靠的但实现起来比较简单。这里主要讲服务端的校验

1、分布式锁解决方案

基于 ZooKeeper 实现分布式锁

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:

  1. 创建一个目录mylock。

  2. 线程A想获取锁就在mylock目录下创建临时顺序节点。

  3. 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁。

  4. 线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点。

  5. 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。

优缺点分析:

  • 具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
  • 因为需要频繁的创建和删除节点,性能上不如Redis方式。

基于 Redis 实现分布式锁

推荐一个Redis官方推荐的Redis锁的框架Redisson。基于 Redis 实现分布式锁的步骤如下:

  1. 用户通过浏览器发起请求,服务端收集数据,并且生成code作为唯一业务字段。
  2. 搭配缓存redis,将code以String类型存入到redis中,并设置超时时间。
  3. 判断是否设置成功,如果设置成功,说明是第一次请求,则进行数据操作。
  4. 如果设置失败,说明是重复请求,则直接返回成功,不再重复进行数据处理。

在这里插入图片描述

问题分析:

场景一:用户快速点击提交,连续发起两次请求。第一次请求先到达服务端,然后第二次请求由于某些原因过了一会儿才到达服务端。等第二次请求达到服务端的时候,第一次请求已经执行完毕并且释放了锁。此时第二次请求仍然能加锁成功,并且执行业务逻辑,这种情况下幂等性失效。

场景二:客户端发起第一次请求,服务端正常执行完毕并释放了分布式锁,但由于网络原因客户端没有正常收到服务端的响应,此时客户端再次发起请求。由于第一次请求所加的分布式锁已经过期所以第二次请求仍然能够加锁成功,然后执行业务逻辑,此时幂等性失效。

场景三:客户端连续发起多次请求,这多次请求同时到达服务端,此时开始争抢锁,谁抢到锁谁就执行,其他没有抢到锁的请求都统统不执行。这种情况能保证幂等性。

综上,这种方式有一定的缺陷性。

2、唯一索引解决方案

方案介绍:

  1. 根据业务需求,对数据库表中的字段设置唯一索引,可以是单一索引,也可以是联合索引。防止新增脏数据。如新增用户数据,一般用户名是唯一的。所以会在用户名字段添加唯一索引。
  2. 在接口做插入操作的时候,第一次请求时数据会插入成功。后续重复的数据插入数据时,会抛出异常提示唯一索引有冲突。此时我们需要对该异常进行捕获,然后返回成功,更加直观地反馈给请求端,而不是直接反馈异常,否则终端也不好识别,以此来保证接口的幂等性。

具体步骤:

  1. 用户通过浏览器发起请求,服务端收集数据。
  2. 将该数据插入mysql。
  3. 判断是否执行成功,如果成功,返回成功的响应数据。
  4. 如果执行失败,要注意区分失败的类型,捕获唯一索引冲突异常DuplicateKeyException,此时直接返回成功。

优缺点分析:

  • 使用起来比较简单,使用SQL脚本建立唯一索引即可,可以使用以下SQL脚本创建索引。
CREATE INDEX index_name ON table_name (column_list);
ALTER TABLE table_name ADD INDEX index_name (column_list);
  • 编码上比较麻烦,因为每个需要保证幂等的插入类型的接口都需要去做捕获DuplicateKeyException异常的操作,代码上比较冗余。
  • 适用面不广,只能适用于插入操作。
  • 效率不高,基于数据库的唯一key去做防重和保证插入幂等,那么相当于把压力放到了数据库上。在高并发的情况下很可能出现性能问题。

综上,这种方式也不适合多场景的使用。

3、乐观锁解决方案

方案介绍:

数据库乐观锁方案一般只能适用于执行更新操作的过程,我们可以提前在对应的数据表中多添加一个字段,充当当前数据的版本标识。这样每次对该数据库该表的这条数据执行更新时,都会将该版本标识作为一个条件,值为上次待更新数据中的版本标识的值。

在这里插入图片描述

  • 每次查询的时候查询出版本号,每次执行更新的时候就带上这个版本号。
  • 服务端接口收到请求后按照版本号去更新数据,每次更新后将版本号 + 1
  • 如果重复发起请求,那么每次请求的version一定是一样的,但是只要有一次更新成功了那么数据库的版本号就+1,可以保证后面的请求更新数据库不会成功。

优缺点分析:

  • 实现简单,只需要在表中增加一个字段即可。
  • 适用面不广,只能适用于更新相关的场景。
  • 效率不高,适用数据库来保证幂等性,这样就是把压力放到数据库去了,本来数据库就是很多项目的性能瓶颈。

综上,这种方式跟数据库唯一key解决方案类似,也不适合多场景的使用。

4、Token解决方案

Token解决方案适用于新增或者修改操作,这种方案由于不依赖于接口内部代码进行判断,所以可以通过拦截器AOP切面 + 注解的方式做得更加通用,仅用一个注解就能让某个接口保证幂等性。

主要分为两个阶段,获取token和使用token。每次接口请求前先获取一个token,然后再下次请求的时候在请求中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。

方案介绍:

在这里插入图片描述

  1. 服务端需要提供一个token获取接口(该 Token 可以是一个序列号,也可以是一个分布式 ID 或者 UUID 串,要求保证唯一性),客户端调用接口获取 Token,这时候服务端会生成一个 Token 。
  2. 然后将该串存入 Redis 数据库中,以该 Token 作为 Redis 的键(注意设置过期时间)。
  3. 将 Token 返回到客户端,客户端拿到后应存到表单隐藏域中。
  4. 客户端在执行提交表单时,把 Token 存入到 Headers 中,执行业务请求带上该 Headers。
  5. 服务端接收请求后从 Headers 获取 Token,然后根据 Token 到 Redis 中查找该 key 是否存在。
  6. 服务端根据 Redis 中是否存该 key 进行判断,如果存在就将该 key 删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。

问题分析:

  1. 这种方案不需要在业务代码里做幂等校验,通过AOP切面 + 注解可以做的非常通用,使用起来很方便。但需要前端多发一次请求去请求token。
  2. 如果客户端连续发起调用,只要每次使用的token是一样的,那么这些连续的请求只会被处理一次。由此可以正常保证幂等性。
  3. 如果某个客户端第一次发起请求,然后服务端收到后将token从Redis中删除,接着去执行业务逻辑,但是业务逻辑执行失败了,此时有两种可能:
  • 如果此时服务端可能会向客户端返回执行失败,客户端收到该返回后自动重新请求一个token,然后再次发起请求重试。这样也没有任何问题。
  • 如果此时服务端向客户端返回执行失败的过程中,由于网络或其他什么原因导致客户端无法接收到服务端返回的执行失败响应。那么此时客户端会再次使用第一次申请的token再次向服务端发送请求,但是此时服务端返回的确却是重复请求或执行成功。

总的来说这种方案实用性较强,没有明显的缺陷。

总结

一般来说没有任何一种幂等方案可以适用于所有场景,需要按照实际情况来选择合适的方案。也可以采用多种方案组合使用来保证接口幂等性,比如使用token方案 + 数据库唯一索引方案组合使用。

在这里插入图片描述

  • 3
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
接口可以通过多种方案解决。其中一种常用的方案是基于唯一索引的数据库方案。具体步骤是,用户通过浏览器发起请求,服务端接收数据,将该数据插入数据库并判断是否执行成功。如果执行成功,则操作其他数据,可能还有其他的业务逻辑。如果执行失败,捕获唯一索引冲突异常,直接返回成功。这种方案使用起来比较简单,只需要确定哪个是唯一key,然后建立唯一索引即可。但是编码上比较麻烦,因为每个需要保证等的插入类型的接口都需要做捕获DuplicateKeyException异常的操作,代码上比较冗余。而且适用面不广,只能适用于插入操作,并且只能适用于简单的业务场景。在高并发的情况下,可能会出现问题,因为将压力放到了数据库上。 另外一种方案是基于Redis的token解决方案。这种方案适用于更新操作和新增操作,特别适合于前端界面和后端接口交互的等方式。这种方案不依赖于接口内部代码进行判断,所以可以通过拦截器或AOP切面注解的方式做的更加通用,仅用一个注解就能让某个接口保证。 除了以上两种方案,还可以使用基于等表的通用等组件来实现。这是一种非常稳定、简单和通用的方案,推荐使用。具体实现细节可以参考链接:https://gitee.com/mr_wenpan/basis-enhance/tree/master/enhance-boot-idempotent 总结来说,接口可以通过基于唯一索引的数据库方案、基于Redis的token解决方案或基于等表的通用等组件来解决。选择哪种方案要根据实际情况来确定。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [接口解决方案](https://blog.csdn.net/Hellowenpan/article/details/120993054)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

霁晨晨晨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值