幂等控制

什么是幂等性:
    抄用一段数学上的定义:f(f(x)) = f(x)。x被函数f作用一次和作用无限次的结果是一样的。幂等性应用在软件系统中,
我把它简单定义为:某个函数或者某个接口使用相同参数调用一次或者无限次,其造成的后果是一样的,在实际应用中一般针对
于接口进行幂等性设计。举个栗子,在系统中,调用方A调用系统B的接口进行用户的扣费操作时,由于网络不稳定,A重试了N次
该请求,那么不管B是否接收到多少次请求,都应该保证只会扣除该用户一次费用。

    幂等性一般应用于协议设计,TCP协议支持幂等吗?答案是肯定的,在网络不稳定时,操作系统可以肆无忌惮的重发TCP报文
片段。TCP协议能够保证幂等的核心在于sequence number字段,一个序列号的在较长的一段时间内均不会出现重复。对于应用
层的协议设计,原理和TCP是类似的,我们需要一个不重复的序列号。再简单一点说,在一个业务流程的处理中,我们需要一个
不重复的业务流水号,以保证幂等性。

    举个实际应用场景:用户A在网页上发起一笔游戏充值请求,浏览器引导用户去银行支付,支付成功后系统给用户进行充值。协议
设计上,我们通过全局唯一的充值订单号贯穿整个业务流程,使该业务支持幂等。

我们先从一个简单的程序理解一下幂等性:
    public class Main {
        private int i = 0;
        //这个方法不具有幂等性,每调用一次,它就会改变Main的状态(即改变了i)
        public void idempotent() {
            i++;
        }
        //幂等性,无论这个方法调用多少次,它都不会改变Main类的状态。
        public void simple() {
            System.out.println(i);
        }
    }
    看完这些,你似乎对幂等性有了更深的了解。那么幂等性问题会出现在哪些场景呢?电商,第三方支付,抢红包等场景。
    这些应用场景,你似乎看到了他们的共同特征。对,那就是高并发。高并发往往伴随着分布式。基于HTTP协议的Web API
是时下最为流行的一种分布式服务提供方式。无论是在大型互联网应用还是企业级架构中,我们都见到了越来越多的SOA或
RESTful的Web API。为什么Web API如此流行呢?我认为很大程度上应归功于简单有效的HTTP协议。HTTP协议是一种分布式
的面向资源的网络应用层协议,无论是服务器端提供Web服务,还是客户端消费Web服务都非常简单。再加上浏览器、Javascript、
AJAX、JSON以及HTML5等技术和工具的发展,互联网应用架构设计表现出了从传统的PHP、JSP、ASP.NET等服务器端动态网页
向Web API + RIA(富互联网应用)过渡的趋势。Web API专注于提供业务服务,RIA专注于用户界面和交互设计,从此两个领域的
分工更加明晰。在这种趋势下,Web API设计将成为服务器端程序员的必修课。然而,正如简单的Java语言并不意味着高质量的
Java程序,简单的HTTP协议也不意味着高质量的Web API。要想设计出高质量的Web API,还需要深入理解分布式系统及HTTP协议
的特性。
    幂等性(Idempotence)。在HTTP/1.1规范中幂等性的定义是:Methods can also have the property of "idempotence"
 in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same 
as for a single request.
    从定义上看,HTTP方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。幂等性属于语义范畴,正如编译器只能
帮助检查语法错误一样,HTTP规范也没有办法通过消息格式等语法手段来定义它,这可能是它不太受到重视的原因之一。但实际上,
幂等性是分布式系统设计中十分重要的概念,而HTTP的分布式本质也决定了它在HTTP中具有重要地位。
    为什么需要幂等性呢?我们先从一个例子说起,假设有一个从账户取钱的远程API(可以是HTTP的,也可以不是),我们暂时用
类函数的方式记为:bool withdraw(account_id, amount)。
    withdraw的语义是从account_id对应的账户中扣除amount数额的钱;如果扣除成功则返回true,账户余额减少amount;
如果扣除失败则返回false,账户余额不变。值得注意的是:和本地环境相比,我们不能轻易假设分布式环境的可靠性。一种典型的
情况是withdraw请求已经被服务器端正确处理,但服务器端的返回结果由于网络等原因被掉丢了,导致客户端无法得知处理结果。
如果是在网页上,一些不恰当的设计可能会使用户认为上一次操作失败了,然后刷新页面,这就导致了withdraw被调用两次,账户
也被多扣了一次钱。如图1所示:
图1
    这个问题的解决方案一是采用分布式事务,通过引入支持分布式事务的中间件来保证withdraw功能的事务性。分布式事务的优点
是对于调用者很简单,复杂性都交给了中间件来管理。缺点则是一方面架构太重量级,容易被绑在特定的中间件上,不利于异构系统的
集成;另一方面分布式事务虽然能保证事务的ACID性质,而但却无法提供性能和可用性的保证。
    另一种更轻量级的解决方案是幂等设计。我们可以通过一些技巧把withdraw变成幂等的,比如:    int create_ticket() ;    bool idempotent_withdraw(ticket_id, account_id, amount);    create_ticket的语义是获取一个服务器端生成的唯一的处理号ticket_id,它将用于标识后续的操作。idempotent_withdraw
和withdraw的区别在于关联了一个ticket_id,一个ticket_id表示的操作至多只会被处理一次,每次调用都将返回第一次调用时的
处理结果。这样,idempotent_withdraw就符合幂等性了,客户端就可以放心地多次调用。
    基于幂等性的解决方案中一个完整的取钱流程被分解成了两个步骤:
    1.调用create_ticket()获取ticket_id;
    2.调用idempotent_withdraw(ticket_id, account_id, amount)。虽然create_ticket不是幂等的,但在这种设计下,
它对系统状态的影响可以忽略,加上idempotent_withdraw是幂等的,所以任何一步由于网络等原因失败或超时,客户端都可以
重试,直到获得结果。如图2所示:
图2
    和分布式事务相比,幂等设计的优势在于它的轻量级,容易适应异构环境,以及性能和可用性方面。在某些性能要求比较高的
应用,幂等设计往往是唯一的选择。
    HTTP的幂等性
    HTTP协议本身是一种面向资源的应用层协议,但对HTTP协议的使用实际上存在着两种不同的方式:一种是RESTful的,它把
HTTP当成应用层协议,比较忠实地遵守了HTTP协议的各种规定;另一种是SOA的,它并没有完全把HTTP当成应用层协议,而是把
HTTP协议作为了传输层协议,然后在HTTP之上建立了自己的应用层协议。本文所讨论的HTTP幂等性主要针对RESTful风格的,不过
正如上一节所看到的那样,幂等性并不属于特定的协议,它是分布式系统的一种特性;所以,不论是SOA还是RESTful的Web API
设计都应该考虑幂等性。下面将介绍HTTP GET、DELETE、PUT、POST四种主要方法的语义和幂等性。
    HTTP GET方法用于获取资源,不应有副作用,所以是幂等的。比如:GET http://www.bank.com/account/123456,不会改变
资源的状态,不论调用一次还是N次都没有副作用。请注意,这里强调的是一次和N次具有相同的副作用,而不是每次GET的结果相同
GET http://www.news.com/latest-news这个HTTP请求可能会每次得到不同的结果,但它本身并没有产生任何副作用,因而是满足
幂等性的。
    HTTP DELETE方法用于删除资源,有副作用,但它应该满足幂等性。比如:DELETE http://www.forum.com/article/4231,
调用一次和N次对系统产生的副作用是相同的,即删掉id为4231的帖子;因此,调用者可以多次调用或刷新页面而不必担心引起错误
    比较容易混淆的是HTTP POST和PUT。POST和PUT的区别容易被简单地误认为“POST表示创建资源,PUT表示更新资源”;而实际上
二者均可用于创建资源,更为本质的差别是在幂等性方面。在HTTP规范中对POST和PUT是这样定义的:
    The POST method is used to request that the origin server accept the entity enclosed in the request as
a new subordinate of the resource identified by the Request-URI in the Request-Line ...... If a resource has been 
created on the origin server, the response SHOULD be 201 (Created) and contain an entity which describes the status
of the request and refers to the new resource, and a Location header.
The PUT method requests that the enclosed entity be stored under the supplied Request-URI. If the 
Request-URI refers to an already existing resource, the enclosed entity SHOULD be considered as a modified 
version of the one residing on the origin server. If the Request-URI does not point to an existing resource, 
and that URI is capable of being defined as a new resource by the requesting user agent, the origin server can 
create the resource with that URI.
    POST所对应的URI并非创建的资源本身,而是资源的接收者。比如:POST http://www.forum.com/articles的语义是在
http://www.forum.com/articles下创建一篇帖子,HTTP响应中应包含帖子的创建状态以及帖子的URI。两次相同的POST请求
会在服务器端创建两份资源,它们具有不同的URI;所以,POST方法不具备幂等性。而PUT所对应的URI是要创建或更新的资源本身。比如:
PUT http://www.forum/articles/4231的语义是创建或更新ID为4231的帖子。对同一URI进行多次PUT的副作用和一次PUT是
相同的;因此,PUT方法具有幂等性。
在介绍了几种操作的语义和幂等性之后,我们来看看如何通过Web API的形式实现前面所提到的取款功能。很简单,用POST/tickets
实现create_ticket;用PUT /accounts/account_id/ticket_id&amount=xxx来实现idempotent_withdraw。值得注意的是严格来讲
amount参数不应该作为URI的一部分,真正的URI应该是/accounts/account_id/ticket_id,而amount应该放在请求的body中。这种模式
可以应用于很多场合,比如:论坛网站中防止意外的重复发帖。
    幂等控制的实现
    幂等表示:请求服务器一次或是多次,返回的结果均是一样的【select 】一般是GET请求
    非幂等表示:请求服务器不同的次数,返回的结果将是不一样的[update   delete] 一般是POST请求
    HTTP协议本身是一种面向资源的应用层协议,但对HTTP协议的使用实际上存在着两种不同的方式:一种是restful,它把HTTP
当成应用层协议,另一种是SOA,它并没有完全把HTTP当成应用层协议,而是把HTTP协议作为了传输层协议,然后在HTTP之上建立了自己的
应用层协议。
    restful风格,想了解的可以去看看webservice编程,这里不是本文的主题。
    本文所讨论的HTTP幂等性主要针对RESTful风格的,不过正如上一节所看到的那样,幂等性并不属于特定的协议,它是分布式
系统的一种特性;所以,不论是SOA还是RESTful的Web API设计都应该考虑幂等性。
 
重要方法 安全 幂等
GET
POST
PUT
DELETE
数据库上的幂等和事务是一体的。    1. 查询操作  查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作     2. 删除操作 删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个)     3.唯一索引,防止新增脏数据 比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录     4.悲观锁 获取数据的时候加锁获取 select * from table_xxx where id='xxx' for update; 注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的 悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用 5. 乐观锁 乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。

客户端幂等控制机制-token 

    业务要求: 页面的数据只能被点击提交一次     发生原因: 由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交     解决办法: 集群环境:采用token加redis(redis单线程的,处理需要排队) 单JVM环境:采用token加redis或token加jvm内存     处理流程: 1. 数据提交前要向服务的申请token,token放到redis或jvm内存,token有效时间 2. 提交后后台校验token,同时删除token,生成新的token返回     token特点: 要申请,一次有效性,可以限流



指导思想

应用分层、工作流分阶段幂等;

接口幂等

分布式系统中,接口幂等性是系统可行性论证的第一个步骤。 
一个软件系统中,所有的接口都可以归结为增删改查四大类;下面我们对这四大类接口进行分析;

查询和删除

查询和删除业务,天然的具有幂等的特性; 
1. 查询操作 
在数据不变的情况下,查询一次和查询多次,查询结果是一样的; 
2. 删除操作 
删除一次和多次删除的结果都是把数据删除;

新增和更新

带有新增和更新的业务接口,如果不做幂等性处理,很可能没调用一次,都会对系统的存储产生影响;

新增业务

新增业务类接口,我们要解决如下两个问题 
1. 同一个用户用同样的数据多次请求同一个接口(不管是什么原因多次提交,他应该只请求一次) 
2. 不同用户的提交同样的数据请求同一个接口; 
第一个问题可以通过防重复提交来解决;业务数据连同Token,一起提交给接口,同一个Token,只能被处理一次(这里要注意,只能被处理一次,应该改成只能被正确的处理一次,也就是说,我们应该缓存某次新增业务处理的结果,如果上一次请求时出现某些异常,比如数据库连接失败,用户再次提交的时候,我们应该放行用户的这次请求,当然有些异常就不需要放行了,比如提交的业务数据不对等); 
第二个问题是无法解决的,一个开放的系统,不能杜绝两个不同的客户端(用户)同时请求;但是可以交给数据的最后防线,存储层;通过唯一索引或唯一组合索引可以防止新增数据存在脏数据 (当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可) ;

注意: 
Token防重复提交,只需要网关这层控制即可;Token的处理机制,还需要缓存调用的处理结果,以判断是否需要放行后续的重试请求;

更新业务

系统中的大部分业务都可以归属到更新业务,比如禁用用户、电商秒杀等等,只要是有更新操作的,不管是不是还有其他的操作,都归结到更新业务; 
更新业务接口,不仅需要有表单防重复提交的验证,还需要有下面这些更精细的控制,以防止高并发环境中出现脏读,幻读等引起错误的数据更新结果; 
更新业务接口幂等性解决方案一般是通过各种层面的锁和CAS机制;

扫描二维码关注公众号,回复: 928662 查看本文章

悲观锁

悲观锁,select for update,整个执行过程中锁定要操作的记录;

乐观锁

更新业务的接口,比如订单付款等,需要综合使用尽可能多的信息来逐步验证逐步减少直至杜绝重复消息重复处理的概率;基本思路是CAS(Compare And Set); 
可以参考下面的两篇文章体会一下: 
1. 《架构师之路-库存扣多了,到底怎么整》 
2. 订单操作,利用订单编号和订单的状态机(序列号)

测试用例

通过下面的方法可以初步验证接口幂等性的健壮性: 
1. 同一个请求,多次提交到同一台节点,多次提交到不同的节点 
2. 同一个请求,同时到达同一个节点,同时到达到不同的节点 
3. 有逻辑先后顺序的消息、请求乱序的处理,比如创建订单的请求和支付订单的请求,不能保证第一个请求先于第二个请求到达服务器;

参考

架构师之路-库存扣多了,到底怎么整 
高并发的核心技术-幂等的实现方案 
分布式系统互斥性与幂等性问题的分析与解决 
基础篇(一)幂等性 
分布式系统—幂等性设计 
分布式系统的接口幂等性设计 
后端接口的幂等性 
API接口非幂等性问题及使用redis实现简单的分布式锁 
GitHub Idempotent 
GitHub Ddripping


猜你喜欢

转载自blog.csdn.net/xiaohanzuofengzhou/article/details/79567522