幂等性设计和解决方案

什么是幂等性

(一)介绍

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

(二)HTTP维度

在 HTTP/1.1规范中幂等性的定义是:
Methods can also have the property of"idempotence"in that(aside from error or expiration issues)the side-effects of N>o identical requests is the same as for a single request.
从定义上看,HTTP方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用.
HTTP请求常见有 GET、DELETE、PUT、POST 四种主要方法;

(1)GET方法

HTTP GET方法用于获取资源,不应有副作用,所以是幂等的.
比如:GET https//ww.XXXX.com/categories/100不会改变资源的状态,不论调用一次1还是N 次都没有副作用.
请注意,这里强调的是一次和N次具有相同的副作用,而不是每次GET的结果相同.
GET htps//wwww.XXXX.com/categories这个HTTIP请求可能会每次得到不同的结果,但它本身并没有产生任何副作用,因而是满足幂等性的.

(2)DELETE方法

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

(3)Post方法

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

(4)PUT方法

HTTP PUT所对应的 URI是要创建或更新的资源.
比如:PUT https//www.XXX.com/article/5231的语义是创建或更新ID为5231的文章,对同一URI进行多次PUT的副作用和一次PUT是相同的,因此PUT方法具有幂等性.

以上是主要针对RESTful风格的HTTP幂等性讨论;
我们知道,HTTP协议是一种面向资源的应用层协议,但对HTTP协议的应用存在两种不同的方式:

第一种是RESTful的,它把HTTP当成应用层协议,遵守HTTP协议的各种规定;

另一种是在HTTP协议之上封装我们的 RPC,没有完全把HTTP当成应用层协议,而是把HTTP协议作为了传输层协议,然后在HTTP之上建立了自己的应用层协议.
那么抛开HTTP协议的规范 幂等性是分布式系统的重要特性所以不论是RESTful的WebAPI设计还是RPC方式的其他API设计都应该考虑幂等性;

(三)应用维度

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

产生幂等性场景

幂等性问题在我们的开发中,分布式、微服务架构中是随处可见的:因网络波动,可能会引起重复请求;
用户重复操作,用户在使用产品时可能会无意的触发多次下单多次交易,甚至没有响应而有意触发多笔交易;
应用使用了失败或超时重试机制(如Nginx重试、RPC重试或业务层重试等)
第三方平台的接口(如:支付成功回调接口),因为异常导致多次异步回调;中间件/应用服务根据自身的特性,也有可能进行重试.
用户双击提交按钮;
页面重复刷新
使用浏览器后退按钮重复之前的操作,导致重复提交表单;使用浏览器历史记录重复提交表单;
浏览器重复的HTTP 请求;
定时任务重复执行;

幂等性在那一层实现

现在都是分布式、微服务的架构,在那一层进行幂等设计,在那一层解决幂等性问题?

在数据访问层实现比较合适

数据访问层的幂等性
幂等性针对分下面两个方面:
读请求
写请求

读请求需要做幂等?很显然是不需要的

写请求呢?涉及到需要做insert、update、delete数据库操作的.肯定是需要的; 那我们可以得出一个结论,即不会改变数据的操作我们可以不做幂等,会改变数据的操作我们就一定要做幂等.

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

(1)insert

对于 insert操作,当我重复插入数据的时候会出现什么情况?这里分两种情况:
自增主键(有幂等性问题)
业务主键 (没有幂等问题,抛异常)
比如:insert into product (id, name, type, price).
假如我的id是自增主键会有问题吗?一定会有,因为会产生多条业务数据相同主键不同的数据.那如果是业务主键呢?即我假设对 name、type、price建立唯一索引,即使我id相同,数据库也会报错了.
不使用自增主键,使用自定义的序列号,比如UUID等

(2)delete

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

相对值删除
绝对值删除

比如:
delete from product_info where id=1234;-幕等的
delete top(10)from product_info;–不是幂等
如果是绝对值删除,重复操作两次是不会出现问题的,但是如果相对值删除,重复操作就是重复删除多次

(3)update

对于update操作,当重复更新数据的时候会出现什么情况?这里其实和删除操作是一样,也需要分两种情况讨论:
相对值删除
绝对值删除
update product info set price=99 where id=1234;–幂等的
update product info set price= price+100 where id=1234;–不是幂等
如果是绝对值修改,重复操作也不会有问题,但是相对值修改,一定会有问题,?会重复修改多次.

(4)select

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

(5)狭义与广义的幂等

以上的所有讨论都是基于单库的,这是狭义上的幂等处理,但是在实际的业务场景中,比如分布式系统中,我们的一次请求可能有多个步骤,那这种跨服务、跨事务请求的幂等处理怎么办呢?也就是广义上的幂等处理怎么办呢?其实这个就需要分布式事务来干这个事;
所以广义上的幂等处理通过分布式事务来解决,狭义上的幂等处理,对于服务分层来说只需要在数据访问层做幂等操作,而对于读写请求幂等处理,select其实我们不用处理,因为从规范层面上来讲,insert 操作你只要要求必须有唯一业务主键,delete 操作在实际业务上是不会被允许的,select 操作又不需要做幂等处理,那唯一需要处理的是 update 操作,但是也很简单,就把相对值修改转换成绝对值修改即可.

幂等性解决方案

(一)前端幂等性的实现(不是可靠的)
(1)按钮只可操作一次

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

(2)token机制

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

(3)使用Post/Redirect/Get模式

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

(4)在 Session 中存放特殊标志

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

(二)后端幂等性的实现
(1)使用唯一索引防止新增脏数据

此方案可以限制重复插入数据,当数据重复时,插入数据库会抛异常,保证不会出现脏数据,这也是一种简单粗暴的办法;

(2)Token+Redis的幂等方案

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

(3)状态机幂等

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

(4)乐观锁实现幂等

如果更新已有数据,可以进行加锁更新,也可以设计表结构时使用乐观锁,通过 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 或者其他条件来实现乐观锁
update table xxx set quality=quality-#subQuality#,version=version-1 where id=xx and version=version
带条件的乐观锁:
update table xx set quality=quality-#subQuality# where quality-#subQual

(5)防重表实现幂等性

需要增加一个表,这个表叫做防重表(防止数据重复的表)

使用唯一主键去做防重表的唯一索引,比如使用订单号 orderNo 做为防重表的唯一索引,每次请求都根据订单号向去重表中插入一条数据,第一次请求查询订单支付状态,当然订单没有支付,进行支付操作,支付前先向防重表中插入该支付的订单号,插入成功说明可以支忖,无论成功与否,执行完后更新订单状态为成功或其他状态,或者是失败,然后可以删除去重表中的数据.后续的订单因为表中唯一索引而插入失败,则返回操作失败,直到第一次的请求操作完成(成功或失败),可以看出防重表作用是加锁的功能;
在这里插入图片描述

(6)select + insert

该方案就是操作之前先查询一下,符合要求再插入,该方案在没有并发的系统中可以解决幂等问题,在单JVM有并发的时候可以JVM加锁来保证幂等性,在分布式环境它是无法保证幂等性,可以使用分布式锁来保证.

(7)分布式锁保证幂等性

在进入方法时,先去获取锁,假如获取到锁,就继续后面的流程.假如没有获取到锁,就等待锁的释放直到获取到锁,当执行完方法时,释放锁,当然,锁要设个超时时间,防止意外没有释放到锁,它可以用来解决分布式系统的幂等性;
常用的分布式锁实现方案是 redis 和zookeeper 等工具.
使用分布式锁类似于防重表,将防重并发放到了缓存中,较为高效.思路相同,同一时间只能完成一次支付请求.

(8)缓冲队列

将请求都快速地接收下来,放入缓冲队列,后续使用异步任务处理队列中的数据,过滤掉重
复的情求,此方案优点是同步改为异步处理,高吞吐,不足是不能及时地返回请求结果,需 要后续轮询处理结果.

(9)全局唯一号实现幂等

比如通过source来源+seq序列号来判断请求是否重复,在并发时只能理一个请求,其它相同并发请求要么返回请求重复,要么等待前面请求执行完成在执行;

原创文章 105 获赞 33 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Octopus21/article/details/105822472