高并发下如何保证接口的幂等性?

幂等性概念:

我使用相同的请求参数,去请求同一个接口,不管请求多少次获取到的响应数据应该是一样的。

一、为什么接口需要满足幂等性

我们先看一个很常见的例子:接口编写和联调的双方,会提前约定好接口规范,明确响应返回的状态是成功还是失败,然后我们就可以根据响应的结果状态处理相应的业务,但是可能还会出现超时或者异常等,对于这些场景,由于不确定是否成功请求了,我们一般都会选择重试。而重试就是我们一开始在幂等概念里面说到的多次执行。
如果接口不满足幂等性,可能会产生脏数据,甚至会影响业务功能,比如我已经支付过了,因为网络等原因,没有及时反馈,你重复多扣了我一笔钱,那肯定是不合理的。
1)上面说到的只是一个实例,有很多场景都会产生接口幂等性的问题:
2)网络波动等异常,未收到反馈后发起重复请求;
3)用户重复操作,比如多次重复提交等;
4)使用了失效或超时重试机制,发起的重试;
5)页面重复刷新;
6)定时任务重复执行;

针对这些场景,为了保证系统的稳定性和业务的完整性,我们肯定需要在原有的基础上进行优化。实现方式有很多种,可以在前端页面使用节流和防抖等技术来控制重复提交,也可以在后端程序校验控制,还可以从数据库角度添加索引等等,结合实际的业务需求和改造的工作量,选择合适的方式,再通过调优和压测来进一步验证。

二、http请求的幂等性要求

说到接口操作,那就离不开数据的增删改查,说到增删改查,那我们就需要先了解几种请求方式 GET、DELETE、POST和PUT。
接下来我们来看一下http请求的幂等性要求:
GET 方法用于获取资源,不应该有副作用,所以是幂等的;

DELETE 方法用于删除资源,有副作用,但是它应该满足幂等性。调用一次和N次对系统产生的副作用是相同的,即删掉同一条或者一批数据。请注意,幂等性并不意味着服务器必须对每个请求以相同的方式进行响应。使用 DELETE 删除某个主键所在行的数据,第一次请求时,我们可能会收到 HTTP 200 状态代码,指示该数据已成功删除。如果我们再次发送此 DELETE 请求,则可能会收到 HTTP 404 作为响应,因为该项目已被删除。第二个请求没有更改服务器状态,因此即使我们得到不同的响应,DELETE操作也是幂等的。

GET 和 DELETE 是比较好理解的,POST 和 PUT 相对来说比较难理解为什么一个是幂等一个非幂等。

POST用于提交请求,可以更新或者创建资源,是非幂等的,因为一次请求添加一份新资源,二次请求则添加了两份新资源,多次请求会产生不同的结果,因此POST不是幂等操作。比如用户进行注册,每次提交都是创建一个新的用户账号,这个时候就用POST。

而PUT用于向指定URL传送更新资源,是幂等的,PUT将A修改为B,它第一次请求时值变为了B,再进行多次此操作,最终的结果还是B,与一次执行的结果是一样的,所以PUT是幂等操作。比如修改用户密码,虽然提交的还是账户名和密码这两个必填参数,但是每次提交都只是更新该用户密码。此时就比较适合使用PUT。

三、幂等性的解决方案

解决办法分为两个方向,一个方向是客户端防止重复调用,一个是服务端进行校验。客户端防止重复提交并不是绝对可靠的,优点是实现起来比较简单。我们这里只讨论服务端的校验。

(1)分布式锁解决方案

方案介绍:

用户通过浏览器发起请求,服务端收集数据,并且生成code作为唯一业务字段。

搭配缓存redis,将code以String类型存入到redis中,并设置超时时间。

判断是否设置成功,如果设置成功,说明是第一次请求,则进行数据操作。

如果设置失败,说明是重复请求,则直接返回成功,不再重复进行数据处理。

问题分析:

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

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

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

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

(2)数据库唯一key解决方案

方案介绍:

根据业务需求,对数据库表中的字段设置唯一索引,可以是单一索引,也可以是联合索引。

在接口做插入操作的时候,第一次请求时数据会插入成功。后续重复的数据插入数据时,会抛出异常提示唯一索引有冲突。此时我们需要对该异常进行捕获,然后返回成功,更加直观地反馈给请求端,而不是直接反馈异常,否则终端也不好识别,以此来保证接口的幂等性。

具体步骤:

用户通过浏览器发起请求,服务端收集数据。

将该数据插入mysql。

判断是否执行成功,如果成功,返回成功的响应数据。

如果执行失败,要注意区分失败的类型,捕获唯一索引冲突异常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 可以是一个序列号,也可以是一个分布式 ID 或者 UUID 串,要求保证唯一性),客户端调用接口获取 Token,这时候服务端会生成一个 Token 串。
然后将该串存入 Redis 数据库中,以该 Token 作为 Redis 的键(注意设置过期时间)。
将 Token 返回到客户端,客户端拿到后应存到表单隐藏域中。
客户端在执行提交表单时,把 Token 存入到 Headers 中,执行业务请求带上该 Headers。
服务端接收到请求后从 Headers 中拿到 Token,然后根据 Token 到 Redis 中查找该 key 是否存在。
服务端根据 Redis 中是否存该 key 进行判断,如果存在就将该 key 删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。
问题分析:
这种方案不需要在业务代码里做幂等校验,通过AOP切面 + 注解可以做的非常通用,使用起来很方便。但需要前端多发一次请求去请求token
如果客户端连续发起调用,只要每次使用的token是一样的,那么这些连续的请求只会被处理一次。由此可以正常保证幂等性。
如果某个客户端第一次发起请求,然后服务端收到后将token从Redis中删除,接着去执行业务逻辑,但是业务逻辑执行失败了,此时有两种可能:
如果此时服务端可能会向客户端返回执行失败,客户端收到该返回后自动重新请求一个token,然后再次发起请求重试。这样也没有任何问题。
如果此时服务端向客户端返回执行失败的过程中,由于网络或其他什么原因导致客户端无法接收到服务端返回的执行失败响应。那么此时客户端会再次使用第一次申请的token再次向服务端发送请求,但是此时服务端返回的确却是重复请求或执行成功。(这里我们再多做一个判断,如果执行重复我们根据业务查一下表中的数据,判断是否已经执行成功了,执行成功则返回成功的标识)
总的来说这种方案实用性较强,没有明显的缺陷。
一般来说没有任何一种幂等方案可以适用于所有场景,我们需要按照我们的实际情况来选择合适的方案即可。我们也可以采用多种方案组合使用来保证幂等性(我们可以使用token方案 + 数据库唯一key方案组合使用)。

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值