幂等性
- 目录:
1.分布式的复杂性
2.幂等
3.幂等举例
4.幂等场景
5.幂等性适用领域
现在稍具规模的网站和大型应用都不再是单机模式,而是分布式应用,基于多机的集群构建的应用,这样服务能力就可以基本实现横向扩容(scale out),不会像单机模式下的纵向扩容(scale up)会受到单机服务能力上限的限制。另外,随着“微服务”概念的火爆,很多应用在构建之初就已经走在了分布式的路线上了,所以就目前行业的发展来看,基于分布式的应用会越来越普遍,甚至变成常态。加上docker这些容器技术的出现,应用分布式化的工具也越来越成熟。
分布式的复杂性
众所周知,构建分布式应用所面临的复杂度远远超出集中式的单一应用,导致复杂性的因素有很多,在此只提其中一点:网络的不可靠性。在单一进程内部,对一个函数的调用,结果只有两种——成功和失败,失败的情况下,调用者可以决定做一些事情弥补。但是在跨进程的调用中,对一个远程(也可以在同一个节点上)进程上运行的函数调用除了会得到成功和失败,还会有第三种的情况——超时,这个现象被称为分布式的三态。这也是困扰分布式应用构建的最核心因素之一,很多分布式应用的复杂度之所以上升这么多也是因为三态之中的超时引起的。
简单看看超时给我们带来的困扰,进程A调用进程B上的函数f,对于成功和失败的结果,相信和单机下一样,进程A都可以进行很好地的处理,因为结果是很明确的。如果进程A调用f之后,在允许的等待最大时间内没有返回结果,就是调用超时了,此时进程A能做什么?其实进程A什么都做不了,因为超时是一个不明确的结果——成功和失败都有可能。详细解释下可能的情况:
成功的情况:进程A把数据通过网络传输到进程B上,f执行成功,通过网络返回执行结果给进程A,可是网络不太好,传输失败了,进程A并 未在指定时间内收到结果,认为超时了。 失败的情况:情况和成功的情况差不多,只是f执行失败了,但是结果依然传输失败,进程A也认为执行超时了。 未执行的情况:进程A的数据发送到进程B所在的节点过程中网络失败 了,或者发送到了进程B所在的机器上,但是进程B没有消费掉在TCP 网络层的数据等等 由此可见,进程A对于超时确实无能为力,有太多的可能存在的情况了。但是分布式协作过程中又必须解决这个问题,不然分布式应用是没意义的,这种情况下,一般会采用让进程A尝试重试——即重复发起之前的调用。但是这样也可能会带来问题,因为超时的那次调用可能已经成功了,再次以同样的参数调用f会不会带来额外的问题?这就引出本文的主角——幂等性。
幂等
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。所谓“影响相同”,不是要求返回值完全相同,而且是指后续多余的调用对系统的数据一致性不造成破坏。对于写入类操作,如果第一次写入是成功的,后续的写入应该抛出异常或者空操作,或者执行了写入但是未对数据造成变化。对于读取类操作,需要保证其实现上是真正的读取,不能在读操作中夹带写操作。
幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数。
用通俗的话讲:就是针对一个操作,不管做多少次,产生效果或返回的结果都是一样的。接口的幂等性实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。有些接口可以天然的实现幂等性,比如查询接口,对于查询来说,你查询一次和两次,对于系统来说,没有任何影响,查出的结果也是一样。
幂等举例:
1.比如前端对同一表单数据的重复提交,后台应该只会产生一个结果
2.比如我们发起一笔付款请求,应该只扣用户账户一次钱,当遇到网络重发或系统bug重发,也应该只扣一次钱
3.比如发送消息,也应该只发一次,同样的短信如果多次发给用户,用户会崩溃
4.比如创建业务订单,一次业务请求只能创建一个,不能出现创建多个订单
幂等场景
需要实现幂等性的典型场景有以下两种:
客户端发起的请求可能需要重试,请求的后端处理需要保证幂等
后端系统使用同步RPC调用或异步消息实现分布式事务,消息的消费者需要保证幂等
可能会发生重复请求或消费的场景,在微服务架构中是随处可见的。以下是几个常见场景:
网络波动:因网络波动,可能会引起重复请求
分布式消息消费:任务发布后,使用分布式消息服务来进行消费
用户重复操作:用户在使用产品时,可能会无意的触发多笔交易,甚至没有响应而有意触发多笔交易
未关闭的重试机制:因开发人员、测试人员或运维人员没有检查出来,而开启的重试机制(如Nginx重试、RPC通信重试或业务层重试等)
广义上的RPC,包括客户端对服务端的api调用、后端系统的内网调用、跨机房调用等。一次RPC大体上包括三个步骤:发送请求、执行过程、接收响应。由于网络传输存在不确定性,导致RPC调用存在一个陷阱,即有可能出现第一、第二步都成功、第三步失败的情况,此时RPC的调用方由于接收不到结果,无法判断被调用方是否已经完成过程调用,只能按失败处理。
通常RPC调用方会针对网络失败进行重试。在上述情况下,如果远端代码不具备幂等性,却进行了重试,将导致系统的数据一致性遭到破坏,本该只执行一次的事务被执行了两次。
对于异步消息的消费者来讲,也有同样的问题。在手动ACK的情况下,消息的处理需要接收消息、处理消息、ACK三步,ACK的失败也会导致相同的问题。
在交易类的系统(比如电商、证券等)中,对非幂等的远程过程进行重试,可能会导致超买超卖,对客户造成经济损失。
互联网应用一般都是提供7*24服务的,而互联网应用本身又是快速迭代,后端系统是随时有可能需要进行发布的。发布等同于一次宕机(进程被kill),这意味着对于互联网应用的后端系统,宕机是常态而非特例。这也是幂等性和重试的必要性来源之一。
幂等性适用领域
试想这样的一种场景:在电商平台上支付后,因为网络原因导致系统提示你支付失败,于是你又重新付款了一次,等完成后检查网银发现被系统扣了两次款,这是一种什么样的体验?
造成上述问题的原因可能有很多,比如第一次付款时实际支付成功,但是信息返回时网络中断导致系统误判;又比如第一次付款的确失败了,但第二次付款时发生意外,导致支付请求被重复发送等等。在一次支付的过程中,每个环节都有可能会发生问题,我们要如何规避这类问题引发的分险?
幂等性是解决这类问题的方案之一,所以在电商,银行,互联网金融等对数据准确性要求很高的领域中,这一特性具有十分重要的地位。