接口幂等性

1.什么是接口幂等性
幂等,英文叫Idempotence,幂等这个词源自于数学,幂等性是数学中的一个概念,常见于抽象代数中,表达的是N次变换与1次变换的结果相同;简单来说就是如果方法调用一次和多次产生的效果是相同的,那摩它就具有幂等性
幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数,这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变,幂等性本身是一个数学概念,在计算机的各个领域都有涉及和借用

HTTP维度
在HTTP/1.1规范中幂等性定义的是:
在这里插入图片描述
从定义上看,HTTP方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用

HTTP请求常见的有GET、DELETE、PUT、POST四种主要方法;

GET方法
HTTP GET方法用于获取资源,不应有副作用,所以是幂等的。

比如:GET https://www.wkcto.com/course/100不会改变资源的状态,不论调用一次还是N次都没有副作用

比如:GET http://www.wkcto.com/course/100不会改变资源的状态,不论调用一次还是N次都没有副作用

请注意,这里强调的是一次和N次具有相同的副作用,而不是每次GET的结果相同。

GET https://www.wkcto.com/course这个HTTP请求可能会每次得到不同的结果,但它本身是并没有任何副作用的,也就是指不多对数据库中的数据产生任何影响,只是单纯的从数据库获取数据,因而是满足幂等性的

DELETE方法

HTTP DELETE方法用于删除资源,有副作用,但是它应该满足幂等性。
比如:DELETE http://www/article/detail/10,调用一次和N次对系统产生的副作用是相同的,即删掉id为10的文章,因此调用者可以多次调用或刷新页面而不必担心引起错误

POST方法
HTTP POST所对应的URI为资源的接收者。
比如:
POST http://www.co/article的语义是在https://www.co/article下发表一篇文章,两次相同的post请求会在服务器端创建两份资源,所以POST不具有幂等性

PUT方法
HTTP PUT所对应的URI是要创建或更新资源。

比如:PUT http://www.wkcto.com/article/5231的语义是创建或更新ID为5231的文章,对同一URI进行多次PUT的副作用和一次PUT是相同的,都是把这条记录给改了,因此PUT方法具有幂等性

以上是主要针对RESTful风格的HTTP幂等性的讨论;
我们知道,http协议是一种面向资源的应用层协议,但对http协议的应用存在两种不同的方式:
一种是restful的,他把http当成应用层协议,遵守http协议的各种规定;
另一种是在HTTP协议之上封装了我们的RPC,没有完全把HTTP当成应用层协议,而是把HTTP协议作为了传输层协议,然后再HTTP之上建立自己的应用层协议,那摩抛开http协议的规范,幂等性是分布式系统的重要特性,所以不论是restful的web api设计还是RPC方式的其他api设计都应该考虑幂等性;

应用维度
幂等性衍生到软件工程中,它的语义是指:函数/接口可以使用相同的参数重复执行,不应该影响系统状态,也不会对系统造成改变。
也就是任意多次执行所产生的的影响均与一次执行所产生的的影响相同;
如果用户对于同一操作发起的一次请求或者多次请求所产生的的影响是一致的,不会因为多次调用(点击)而产生了副作用;
第一次请求的时候对资源产生了副作用,但是以后的多次请求都不会再对资源产生副作用,这里的副作用是指不会对结果产生破坏或者产生不可预估的结果。及幂等性=多次执行无副作用

2.产生幂等性场景

幂等性问题在我们的开发中,分布式、微服务架构中是随处可见的:
因网络波动,可能会引起重复的请求;
用户重复操作,用户在使用产品时可能会无意的触发多次下单交易,甚至没有响应而有意触发多次交易;
应用使用了失败或超时重试机制(如Nginx重试、RPC重试或业务层重试等)。

第三方平台的接口(如: 支付成功回调接口),因为异常导致多次异步回调;
中间件/应用服务根据自身的特性,也有可能进行重试
用户双击提交按钮;
页面重复刷新;
使用浏览器后退按钮重复之前的操作,导致重复提交表单;
使用浏览器历史记录重复提交表单;
浏览器重复的HTTP请求;
定时任务重复执行;

3.幂等性在哪一层实现
我们现在的项目架构一般都是分布式、微服务的架构,那摩在开发中在哪一层进行幂等设计呢?

经过一番分析,在数据访问层实现是比较合适的;

4.数据层的幂等性
数据访问层主要分为读请求写请求
读请求需要做幂等?很显然是不需要的;那摩写请求呢?涉及到需要做insert、update、delete数据库操作的,肯定是需要的;
那我们可以得出来一个结论,即不会改变数据的操作我们可以不做幂等,会改变数据的操作我们就一定要做幂等;

那我们逐个讨论写请求:insert、delete、update操作,首先我假设我没有做任何应用层面上的幂等操作。

insert:
对于insert操作,当我重复插入数据的时候会出现什么情况?这里分为两种情况:

  • 自增主键 (有幂等性问题)
  • 业务主键 (没有幂等性问题)

比如:insert into product_info(id,name,type,price,tm); 假如我的id是自增主键会有问题吗?显然一定会有幂等问题,因为会产生多条业务数据相同,但是主键不同的数据。
那如果是业务主键呢?即我假设对name、type、price建立唯一索引,这样就ok了,即使我id相同,数据库也会报错,但是这里是句玩笑话,你这样加索引的话可能第二天就会把你辞退,看着办吧。

delete
对于delete操作,当重复执行的时候会出现什么情况?这里也要分为两种情况:

  • 绝对值删除: delete from product_info where id = 1234; — 幂等的
  • 相对值删除: delete top(10) from product_info – 不是幂等(这里假设这条语句的意思是删除排名前10的数据)

如果是绝对值删除,重复操作两次是不会出现问题的,但是如果是相对值删除,重复操作就是重复删除多次

update
对于update操作,当重复更新数据的时候会出现什么情况?这里其实和删除操作是一样的,也需要分两种情况讨论:
相对值删除

绝对值删除

我们拿一个具体的例子分析:
update product_info set price = 99 where id = 1234; --(绝对值修改)幂等的
update product_info set price = price + 100 where id = 1234; – (相对值修改)不是幂等的

如果是绝对值修改,重复操作也不会有问题,但是相对值修改,一定会有问题,会重复修改多次,导致每一次price的值都会发生变化

select
最后是select操作,其实这个不用讨论,因为不会对数据发生的改变的操作我们不用做幂等

狭义与广义的幂等
以上的所有的讨论都是基于单库的,这是狭义上的幂等处理,但是在实际的业务场景中,比如分布式系统中,我们的一次请求可能有多个步骤,这种跨服务、跨事务请求的幂等处理怎么办呢?也就是广义上的幂等处理怎么办呢?其实这个就需要分布式事务来干这个事;

所以广义上的幂等处理通过分布式事务来解决,狭义上的幂等处理,对于服务分层来说只需要在数据访问层做幂等操作,而对于读写请求幂等处理,select其实我们不用处理,因为从规范层面上来讲,insert操作你只要要求必须有唯一的业务主键,delete操作在实际业务上是不会被允许的,select操作又不需要做幂等处理,那唯一需要处理的是update操作,但是也很简单,就把相对值转换为绝对值修改即可。 。。。

保证幂等性的方法
前端幂等性的实现(不是可靠的)

按钮只可操作一次,一般是提交后把按钮置灰或者loading状态,按钮置灰或loading状态可以使用一些js组件实现,消除用户因为重复点击而产生的副作用,比如添加操作,由于点击两次而产生的两条记录。

token机制
产品上允许重复提交,但要保证重复提交不产生副作用,比如点击n次只产生一条记录;具体实现就是进入页面时申请一个token,然后后面所有的请求带上这个token,根据token来避免重复请求;

使用Post/Redirect/Get模式
在提交后执行页面重定向,这就是所谓的Post-Redirect-Get(PRG)模式,简言之,当用户提交了表单后,去执行一个客户端的重定向,转而提交成功信息页面,这样避免用户按f5刷新导致的重复提交,而其也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样重复提交的问题;

在Session中存放特殊标志
在服务器端,生成一个唯一的标识符,将它存入session,同时将他写入表单的隐藏中,然后将表单页面发给浏览器,用户输入信息后点击提交,在服务器端,获取表单中隐藏的字段的值,与session中的唯一标识符相比较,相等说明是首次提交,就处理本次请求,然后将session中的唯一标识符移除,不相等则表示重复提交,不再做处理;

后端幂等性的实现
使用唯一索引防止幂等性问题,此方案可以限制重复插入数据,当数据重复时,插入数据库会抛异常,保证不会出现脏数据,这也是一种简单粗暴的办法;

Token+Redis的幂等方案

这种方式分成两个阶段:申请token阶段和业务操作阶段。
以支付系统为例:
第一阶段,在进入到提交订单页面之前,需要订单系统根据用户信息向支付系统发起一次申请token的请求,支付系统将token保存到Redis缓存中,为第二阶段支付使用。
第二阶段,订单系统拿着申请到的token发起支付请求,支付系统会检查Redis中是否存在token,如果存在,表示第一次发起支付请求,开始支付逻辑处理,处理完逻辑之后删除redis中的token;
当重复请求时候,检查缓存中token不存在,表示非法请求

粗略流程图如下图所示:

在这里插入图片描述
该方案的不足之处是需要与系统间交互两次;

状态机幂等
针对更新操作,比如业务上需要修改订单状态,订单有待支付、支付中、支付成功、支付失败、订单超时关闭等,在设计的时候最好只支持状态的单向改变(不可逆),这样在更新的时候where条件里可以加上status = 我期望的原来的status,多次调用的话实际上也只会执行一次

乐观锁实现幂等
如果更新已有数据,可以进行加锁更新,也可以设计表结构时使用乐观锁,通过version来做乐观锁,这样既能保证执行效率,又能保证幂等,乐观锁的version版本在更新业务数据要自增。
1.查询数据,得到版本号;version=1
2.通过版本号去更新,版本号匹配就更新,版本号不匹配就不能更新;
update xxx set money = money - 99,version = version + 1 where id = xx and version = 1;

也可以采用update with condition,更新带条件,实现乐观锁,通过version或者其他条件来实现乐观锁

支付宝幂等解决方案
每年支付宝在双11和双12的活动中,都展示了绝佳的技术能力。这个能力不但体现在处理高TPS量的访问,更体现在几乎不会出错,不会出现重复支付的情况,那这个是怎么做到的呢?

诚然,为了实现在高并发下仍不会出错的技术目标,支付宝下了很多功夫,比如幂等性的处理,分布式事务的使用等等,但是个人觉得其中最关键的一点就是“一锁二判三更新”这句看似毫不起眼的口诀。

何为“一锁二判三更新”? 简单来说就是当任何一个并发请求过来的时候

我们先锁定关联单据
然后判断关联单据状态,是否之前已经更新过对应状态了
如果基于第2步判断,之前并没有请求更新过对应状态,则本次请求可以更新并完成相关业务逻辑。如果之前已经有更新过状态了,则本次不能更新,也不能完成业务逻辑。
话不多说,我们直接上代码:

//第1步锁当前支付单
PaymentInfo resultPaymentInfo = commonPayCoreService
  .queryPaymentForUpdate(createPaymentInfo.getId());
if (resultPaymentInfo.isFinalStatus()) {
      //第2步,判断当前支付单状态,如果是终态,则直接返回
       //不做任何更新
      return resultPaymentInfo;
}
//第3步更新当前支付单状态到终态,并完成相关业务逻辑(支付成功)
 payCoreService.updateRequestResult(payChannelResult);

基于以上方案可以100%确保在并发情况下不会出现重复更新问题,按理论来说,就是每次状态机变更前,都要在并发安全情况下判断状态是否已经发生过变更了。

总结:

想要保证幂等性,最简单的做法就是:在做业务操作之前,先查一下,判断下本次操作是否有被执行过,如果执行过,则不再执行,否则继续执行。

但是,这个方案存在一个关键性的问题,那就是在高并发场景中,是可能会有幂等击穿的。

在这里插入图片描述
所以,想要解决好这个问题,需要做好并发控制,那么,做并发控制,大家首先想到的就是锁,没错。就是要用锁。

那么,解决幂等问题,请记住这个口诀:”一锁、二判、三更新”

一锁、二判、三更新
“一锁、二判、三更新”,只要严格遵守这个过程,那么就可以解决并发问题。

一锁:第一步,先加锁。可以加分布式锁、或者悲观锁都可以。但是一定要是一个互斥锁!

二判:第二步,进行幂等性判断。可以基于状态机、流水表、唯一性索引等等进行重复操作的判断。

三更新:第三步,进行数据的更新,将数据进行持久化。

在这里插入图片描述

三步需要严格控制顺序,确保加锁成功后进行数据查询和判断,幂等性判断通过后再更新,更新结束后释放锁。

以上操作需要有一个前提,那就是第一步加锁、和第二步判断的时候,需要有一个依据,这个就是幂等号了,通常需要和上游约定一个唯一ID作为幂等号。然后通过对幂等号加锁,再通过幂等号进行幂等判断即可。

一锁这个过程,建议使用Redis实现分布式锁,因为他是非阻塞的高效率的互斥锁。非常适合在幂等控制场景中。

二判这个过程,如果有操作流水,建议基于操作流水做幂等,并将幂等号作为唯一性约束,确保唯一性。如果没有流水,那么基于状态机也是可以的。

但是不管怎么样,数据库的唯一性约束都要加好,这是系统的最后一道防线。万一前面的锁失效了,这里也能控制得住不会产生脏数据。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
分布式接口幂等性问题是指在分布式系统中,由于网络延迟、重试机制等原因,可能导致同一个请求被重复处理,从而产生重复的业务逻辑。为了解决这个问题,需要保证接口幂等性。 保证接口幂等性的方法有多种。一种常见的方法是使用唯一标识来标识每一次请求,比如订单id、支付流水号或者前端生成的唯一随机串。在每次请求之前,需要将唯一标识存放到数据库或者缓存中。后端服务在处理请求之前,需要先检查这个唯一标识是否存在,如果存在,则判定此次请求已经处理过,不需要进行重复处理。这样可以避免重复的业务逻辑。 在分布式场景中,由于负载均衡算法的原因,可能会导致同一个请求被多台机器处理。为了解决这个问题,可以使用分布式锁来保证只有一个机器能够处理该请求。另外,使用分布式事务也可以保证接口幂等性。 此外,还可以通过拦截器(AOP)和注解的方式实现一个通用的解决方案,避免每次请求都写重复的代码。在设计系统时,幂等性是一个需要首要考虑的问题,特别是在涉及到金融交易等关键业务的系统中。 综上所述,保证分布式接口幂等性可以通过使用唯一标识、分布式锁、分布式事务等方法来实现。这样可以避免重复的业务逻辑和数据不一致的问题。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* [分布式环境下接口幂等性浅析](https://blog.csdn.net/ice24for/article/details/86084613)[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^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [分布式开发(二)---接口幂等性(防止重复提交)](https://blog.csdn.net/icanlove/article/details/117652662)[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^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值