在数学里,幂等有两种主要的定义:- 在某二元运算下,幂等元素是指被本身重复运算(或对于函数是为复合)的结果等于它本身的元素。例如,乘法下惟一两个幂等实数为0和1。即 s *s = s- 某一元运算为幂等的时,其做用在任一元素两次后会和其做用一次的结果相同。例如,高斯符号即是幂等的,即f(f(x)) = f(x)。java
在HTTP/1.1规范中幂等性的定义是:mysql
A request method is considered "idempotent" if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request. Of the request methods defined by this specification, PUT, DELETE, and safe request methods are idempotent.redis
HTTP的幂等性指的是一次和屡次请求某一个资源应该具备相同的反作用。如经过PUT接口将数据的Status置为1,不管是第一次执行仍是屡次执行,获取到的结果应该是相同的,即执行完成以后Status =1。sql
在HTTP规范中定义GET,PUT和DELETE方法应该具备幂等性。数据库
The GET method requests transfer of a current selected representation for the target resource,GET is the primary mechanism of information retrieval and the focus of almost all performance optimizations. Hence, when people speak of retrieving some identifiable information via HTTP, they are generally referring to making a GET request.express
GET方法是向服务器查询,不会对系统产生反作用,具备幂等性(不表明每次请求都是相同的结果)bash
The PUT method requests that the state of the target resource be created or replaced with the state defined by the representation enclosed in the request message payload.服务器
也就是说PUT方法首先判断系统中是否有相关的记录,若是有记录则更新该记录,若是没有则新增记录。网络
The DELETE method requests that the origin server remove the association between the target resource and its current functionality. In effect, this method is similar to the rm command in UNIX: it expresses a deletion operation on the URI mapping of the origin server rather than an expectation that the previously associated information be deleted.架构
DELETE方法是删除服务器上的相关记录。
如今简化为这样一个系统,用户购买商品的订单系统与支付系统;订单系统负责记录用户的购买记录已经订单的流转状态(orderStatus),支付系统用于付款,提供
boolean pay(int accountid,BigDecimal amount) //用于付款,扣除用户的
接口,订单系统与支付系统经过分布式网络交互。
这种状况下,支付系统已经扣款,可是订单系统由于网络缘由,没有获取到确切的结果,所以订单系统须要重试。由上图可见,支付系统并无作到接口的幂等性,订单系统第一次调用和第二次调用,用户分别被扣了两次钱,不符合幂等性原则(同一个订单,不管是调用了多少次,用户都只会扣款一次)。若是须要支持幂等性,付款接口须要修改成如下接口:
boolean pay(int orderId,int accountId,BigDecimal amount)
经过orderId来标定订单的惟一性,付款系统只要检测到订单已经支付过,则第二次调用不会扣款而会直接返回结果:
在不一样的业务中不一样接口须要有不一样的幂等性,特别是在分布式系统中,由于网络缘由而未能获得肯定的结果,每每须要支持接口幂等性。
随着分布式系统及微服务的普及,由于网络缘由而致使调用系统未能获取到确切的结果从而致使重试,这就须要被调用系统具备幂等性。例如上文所阐述的支付系统,针对同一个订单保证支付的幂等性,一旦订单的支付状态肯定以后,之后的操做都会返回相同的结果,对用户的扣款也只会有一次。这种接口的幂等性,简化到数据层面的操做:
update userAmount set amount = 'value' ,paystatus = 'paid' where orderId= 'orderid' and paystatus = 'unpay'
其中value是用户要减小的订单,paystatus表明支付状态,paid表明已经支付,unpay表明未支付,orderid是订单号。在上文中提到的订单系统,订单具备本身的状态(orderStatus),订单状态存在必定的流转。订单首先有提交(0),付款中(1),付款成功(2),付款失败(3),简化以后其流转路径如图:
当orderStatus = 1 时,其前置状态只能是0,也就是说将orderStatus由0->1 是须要幂等性的
update Order set orderStatus = 1 where OrderId = 'orderid' and orderStatus = 0
当orderStatus 处于0,1两种状态时,对订单执行0->1 的状态流转操做应该是具备幂等性的。这时候须要在执行update操做以前检测orderStatus是否已经=1,若是已经=1则直接返回true便可。
可是若是此时orderStatus = 2,再进行订单状态0->1 时操做就没法成功,可是幂等性是针对同一个请求的,也就是针对同一个requestid保持幂等。
这时候再执行
update Order set orderStatus = 1 where OrderId = 'orderid' and orderStatus = 0
接口会返回失败,系统没有产生修改,若是再发一次,requestid是相同的,对系统一样没有产生修改。
在微服务架构下,咱们在完成一个订单流程时常常遇到下面的场景:
- 一个订单建立接口,第一次调用超时了,而后调用方重试了一次
- 在订单建立时,咱们须要去扣减库存,这时接口发生了超时,调用方重试了一次
- 当这笔订单开始支付,在支付请求发出以后,在服务端发生了扣钱操做,接口响应超时了,调用方重试了一次
- 一个订单状态更新接口,调用方连续发送了两个消息,一个是已建立,一个是已付款。可是你先接收到已付款,而后又接收到了已建立
- 在支付完成订单以后,须要发送一条短信,当一台机器接收到短信发送的消息以后,处理较慢。消息中间件又把消息投递给另一台机器处理
以上问题,就是在单体架构转成微服务架构以后,带来的问题。固然不是说单体架构下没有这些问题,在单体架构下一样要避免重复请求。可是出现的问题要比这少得多。
为了解决以上问题,就须要保证接口的幂等性,接口的幂等性实际上就是接口可重复调用,在调用方屡次调用的状况下,接口最终获得的结果是一致的。有些接口能够自然的实现幂等性,好比查询接口,对于查询来讲,你查询一次和两次,对于系统来讲,没有任何影响,查出的结果也是同样。
除了查询功能具备自然的幂等性以外,增长、更新、删除都要保证幂等性。那么如何来保证幂等性呢?
若是使用全局惟一ID,就是根据业务的操做和内容生成一个全局ID,在执行操做前先根据这个全局惟一ID是否存在,来判断这个操做是否已经执行。若是不存在则把全局ID,存储到存储系统中,好比数据库、redis等。若是存在则表示该方法已经执行。
从工程的角度来讲,使用全局ID作幂等能够做为一个业务的基础的微服务存在,在不少的微服务中都会用到这样的服务,在每一个微服务中都完成这样的功能,会存在工做量重复。另外打造一个高可靠的幂等服务还须要考虑不少问题,好比一台机器虽然把全局ID先写入了存储,可是在写入以后挂了,这就须要引入全局ID的超时机制。
使用全局惟一ID是一个通用方案,能够支持插入、更新、删除业务操做。可是这个方案看起来很美可是实现起来比较麻烦,下面的方案适用于特定的场景,可是实现起来比较简单。
这种方法适用于在业务中有惟一标的插入场景中,好比在以上的支付场景中,若是一个订单只会支付一次,因此订单ID能够做为惟一标识。这时,咱们就能够建一张去重表,而且把惟一标识做为惟一索引,在咱们实现时,把建立支付单据和写入去去重表,放在一个事务中,若是重复建立,数据库会抛出惟一约束异常,操做就会回滚。
这种方法插入而且有惟一索引的状况,好比咱们要关联商品品类,其中商品的ID和品类的ID能够构成惟一索引,而且在数据表中也增长了惟一索引。这时就可使用InsertOrUpdate操做。在mysql数据库中以下:
insert into goods_category (goods_id,category_id,create_time,update_time)
values(#{goodsId},#{categoryId},now(),now()) on DUPLICATE KEY UPDATE update_time=now()
这种方法适合在更新的场景中,好比咱们要更新商品的名字,这时咱们就能够在更新的接口中增长一个版本号,来作幂等
boolean updateGoodsName(int id,String newName,int version);
在实现时能够以下
update goods set name=#{newName},version=#{version} where id=#{id} and version<${version}
这种方法适合在有状态机流转的状况下,好比就会订单的建立和付款,订单的付款确定是在以前,这时咱们能够经过在设计状态字段时,使用int类型,而且经过值类型的大小来作幂等,好比订单的建立为0,付款成功为100。付款失败为99
在作状态机更新时,咱们就这能够这样控制
update `order` set status=#{status} where id=#{id} and status<#{status}
以上就是保证接口幂等性的一些方法。
转自:https://www.jianshu.com/p/b5205e58b1ca