1. 什么是幂等性
1.1 幂等的来源
幂等来源于数学的概念,在数学的概念里,大概分为以下两种概念:
- 在一个二元运算下,幂等元素是指被自己重复运算(或对于函数式为复合)的结果等于他自己的元素。例如,在乘法下下唯一的两个幂等实数为0和1.即s=s*s。
- 在一元运算为幂等的时候,其作用在任一元素两次后会和其作用一次的结果相同。例如高斯符号便是幂等的,即f(x)=f(f(x))。
1.2 在http中幂等的规范
HTTP的幂等性指的是一次和多次请求某一个资源应该具有相同的副作用。如通过PUT接口将数据的Status置为1,无论是第一次执行还是多次执行,获取到的结果应该是相同的,即执行完成之后Status =1。2. 接口设计是需要幂等性的
首先看看http的幂等接口:
- GET方法是向服务器查询,不会对系统产生副作用,具有幂等性(不代表每次请求都是相同的结果)
- 也就是说PUT方法首先判断系统中是否有相关的记录,如果有记录则更新该记录,如果没有则新增记录。
- DELETE方法是删除服务器上的相关记录。
下面详解说一下:
HTTP协议本身是一种面向资源的应用层协议,但对HTTP协议的使用实际上存在着两种不同的方式:
一种是RESTful的,它把HTTP当成应用层协议,比较忠实地遵守了HTTP协议的各种规定;
另一种是SOA的,它并没有完全把HTTP当成应用层协议,而是把HTTP协议作为了传输层协议,然后在HTTP之上建立了自己的应用层协议。
HTTP GET方法用于获取资源,不应有副作用,所以是幂等的。
HTTP DELETE方法用于删除资源,有副作用,但它应该满足幂等性。
比较容易混淆的是HTTP POST和PUT。POST和PUT的区别容易被简单地误认为“POST表示创建资源,PUT表示更新资源”;
而实际上,二者均可用于创建资源,更为本质的差别是在幂等性方面。在HTTP规范中对POST和PUT是这样定义的:
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中。这种模式可以应用于很多场合,比如:论坛网站中防止意外的重复发帖。
关联接口的实际业务
现在来一个商城支付系统的概念系统,从用户下单到支付系统白条支付、在调用户的账务;订单系统和支付系统和白条账务系统,都有流转状态。下面看看接口:
boolean pay(int accountid,BigDecimal amount) //用于付款,扣除用户的
这种情况下,支付系统已经扣款,但是订单系统因为网络原因,没有获取到确切的结果,因此订单系统需要重试。
由上图可见,支付系统并没有做到接口的幂等性,订单系统第一次调用和第二次调用,用户分别被扣了两次钱,造成重复扣款,不符合幂等性原则(同一个订单,无论是调用了多少次,用户都只会扣款一次)。
如果需要支持幂等性,付款接口需要修改为以下接口:
boolean pay(int orderId,int accountId,BigDecimal amount)
通过orderId来标定订单的唯一性,付款系统只要检测到订单已经支付过,则第二次调用不会扣款而会直接返回结果:
在不同的业务中不同接口需要有不同的幂等性,特别是在分布式系统中,因为网络原因而未能得到确定的结果,往往需要支持接口幂等性。
3. 分布式系统接口的幂等性
随着分布式系统以及微服务的普及,一个系统被横向和纵向切分成若干应用,部署成多台服务,这样因为网络原因、系统瓶颈、并发问题、服务器故障、超时等等原因而未获取都确切结果的,者就需要被调用的系统具有幂等性。例如上文所阐述的支付系统,针对同一个订单保证支付的幂等性,一旦订单的支付状态确定之后,以后的操作都会返回相同的结果,对用户的扣款也只会有一次。这种接口的幂等性,简化到数据层面的操作:
update userAmount set amount = 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是相同的,对系统同样没有产生修改。
4. 电商系统中的应用
电商的很多业务,考虑更多的是 BASE(即Basically Available、Soft state、和Eventually consistent),而不是 ACID(Atomicity、Consistency、Isolation和 Durability)。
即为了满足高负载的用户访问,我们可以容忍短暂的数据不一致。
那怎么做呢?
第一,不做分布式事务,代价太大。第二,不一定需要实时一致性,只需要保证最终的一致性即可。
第三,“通过状态机和严格的有序操作,来最大限度地降低不一致性”。
第四,最终一致性(Eventually Consistent)通过异步事件做到。 如果消息具有操作幂等性,也就是一个消息被应用多次与应用一次产生的效果是一样的话,那么把不需要同步执行的事务交给异步消息推送和订阅者集群来处理即可。假如消息处理失败,那么就消息重播,由于幂等性,应用多次也能产生正确的结果。
实际情况下,消息很难具有幂等性,解决方法是使用另一个表记录已经被成功应用的消息,即消息队列和消息应用状态表一起来解决问题。 PS: 最佳结果是引入一个“第三方”的通知/消息机制,比如文中提到的ticket亦或者异步消息推送,而业界传闻很久的淘宝notify框架大概也就是以这为基石创建的吧。
系统越来越SOA所导致的分布式锁的问题是目前所有web开发人员面临的问题,纯粹靠程序事务以及数据库事务是无法解决的,倒是最终一致性很巧妙的解决了该问题。
使用同一节点事务加上消息机制可确保即使系统发生故障也可确保最终一致性,同时又保证了性能和可伸缩性。