欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

幂等性的理解

程序员文章站 2024-03-25 19:25:52
...

网络上对幂等性的解释类似于:

"幂等性是指重复使用同样的参数调用同一方法时总能获得同样的结果。比如对同一资源的GET请求访问结果都是一样的。"

这种解释是非常错误的, 幂等性强调的是外界通过接口对系统内部的影响,

幂等性(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方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用

System.getCPULoad(), 这两次调用返回能一样吗? 但因为是只读接口, 对系统内部状态没有影响, 所以这个函数还是幂等性的.

首先了解一下什么是幂等性

幂等(idempotence)是来自于高等代数中的概念。


单目运算, x为某集合内的任意数, f为运算子如果满足f(x)=f(f(x)), 那么我们称f运算为具有幂等性(idempotent)

比如在实数集中,绝对值运算就是一个例子: abs(a)=abs(abs(a))

双目运算,x为某集合内的任意数, f为运算子如果满足f(x,x)=x, f运算的前提是两个参数都同为x, 那么我们也称f运算为具有幂等性

比如在实数集中,求两个数的最大值的函数: max(x,x) = x, 还有布尔代数中,逻辑运算 "与", "或" 也都是幂等运算, 因为他们符合AND(0,0) = 0, AND(1,1) = 1, OR(0,0) = 0, OR(1,1) = 1

在将幂等性应用到软件开发中,需要一些更深的理解. 我的理解如下:

数学处理的是运算和数值, 程序开发中往往处理的是对象和函数. 但是我们不能简单地理解为数学幂等中的运算就是函数,而数值就是对象!!

比如有Person对象有两个属性weight和age,但是所有的function只能对其中一个属性操作. 所以从这个层面我们可以理解为: 函数只对该函数所操作的对象某个属性具有幂等性, 而不是说对整个对象有运算幂等性.

  1. Person {  
  2.  private int weight;  
  3.  private int age;  
  4.  //是幂等函数  
  5.  public void setAge(int v){  
  6.      this.age = v;   
  7.  }  
  8.  //不是幂等函数  
  9.  public void increaseAge(){  
  10.      this.age++;  
  11.  }   
  12.  //是幂等函数  
  13.  public void setWeight(int v){  
  14.      this.weight=v+10;//故意加10斤!!  
  15.  }  
  16. }  

Person {
 private int weight;
 private int age;
 //是幂等函数
 public void setAge(int v){
     this.age = v; 
 }
 //不是幂等函数
 public void increaseAge(){
     this.age++;
 } 
 //是幂等函数
 public void setWeight(int v){
     this.weight=v+10;//故意加10斤!!
 }
}

还有一点必须要澄清的是: 幂等性所表达的概念关注的是数学层面的运算和数值, 并没有提及到数值的安全性问题.

比如上面的Person的setAge函数, 有两种情况不是幂等性所关心的, 但程序开发却又必须要关心的:

1. 两个线程同时调用

2. 因为age从业务上讲不可能递减, 如果前一次调用设置是30岁, 后一次调用变成了10岁或是更离谱的 -1 岁

所以RESTful设计中将幂等性和安全性是作为两个不同的指标来衡量POST,PUT,GET,DELETE操作的:

重要方法 安全? 幂等?
GET
DELETE
PUT
POST

幂等性是系统的接口对外一种承诺(而不是实现), 承诺只要调用接口成功, 外部多次调用对系统的影响是一致的. 声明为幂等的接口会认为外部调用失败是常态, 并且失败之后必然会有重试.

value getValue(key){
    value = getValueFromCache(key);

    if( value == null ){
        value = readFromPersistence(key);
        saveValueIntoCache(key,value);
    }

    return value;
}
幂等外部调用范式
  1. client.age = 30;  
  2.   
  3. while(一些退出条件){  
  4.     try{  
  5.         if(socket.setPersonAge(person,client.age) == FAILED){  
  6.             int newAge = socket.getPersonAge();  
  7.             //处理冲突问题: 因为age只可能越来越大,所以将client的age更新为server端更大的age  
  8.             if(newAge>30){  
  9.             client.age = newAge;  
  10.             break;  
  11.         } else{  
  12.         <span style="white-space: pre; ">   </span>//无法进行冲突解决,再次尝试  
  13.         }  
  14.         } else return;  
  15.     } catch(Exception){  
  16.         //发生网络异常, 再次尝试  
  17.     }  
  18. }  

client.age = 30;

while(一些退出条件){
    try{
        if(socket.setPersonAge(person,client.age) == FAILED){
	        int newAge = socket.getPersonAge();
	        //处理冲突问题: 因为age只可能越来越大,所以将client的age更新为server端更大的age
	        if(newAge>30){
			client.age = newAge;
			break;
		} else{
			//无法进行冲突解决,再次尝试
		}
        } else return;
    } catch(Exception){
        //发生网络异常, 再次尝试
    }
}

幂等接口的内部实现需要有对内保护机制, 一般情况是用类似于乐观锁的版本机制


为什么需要幂等性呢?我们先从一个例子说起,假设有一个从账户取钱的远程API(可以是HTTP的,也可以不是),我们暂时用类函数的方式记为:

bool withdraw(account_id, amount)

withdraw的语义是从account_id对应的账户中扣除amount数额的钱;如果扣除成功则返回true,账户余额减少amount;如果扣除失败则返回false,账户余额不变。值得注意的是:和本地环境相比,我们不能轻易假设分布式环境的可靠性。一种典型的情况是withdraw请求已经被服务器端正确处理,但服务器端的返回结果由于网络等原因被掉丢了,导致客户端无法得知处理结果。如果是在网页上,一些不恰当的设计可能会使用户认为上一次操作失败了,然后刷新页面,这就导致了withdraw被调用两次,账户也被多扣了一次钱。


这个问题的解决方案一是采用分布式事务,通过引入支持分布式事务的中间件来保证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是幂等的,所以任何一步由于网络等原因失败或超时,客户端都可以重试,直到获得结果。

综上所述

和分布式事务相比,幂等设计的优势在于它的轻量级,容易适应异构环境,以及性能和可用性方面。在某些性能要求比较高的应用,幂等设计往往是唯一的选择

另外;HTTP协议本身是一种面向资源的应用层协议,但对HTTP协议的使用实际上存在着两种不同的方式:一种是RESTful的,它把HTTP当成应用层协议,比较忠实地遵守了HTTP协议的各种规定;另一种是SOA的,它并没有完全把HTTP当成应用层协议,而是把HTTP协议作为了传输层协议,然后在HTTP之上建立了自己的应用层协议。本文所讨论的HTTP幂等性主要针对RESTful风格的,不过正如上一节所看到的那样,幂等性并不属于特定的协议,它是分布式系统的一种特性;所以,不论是SOA还是RESTful的Web API设计都应该考虑幂等性