原文地址:聊聊开发中幂等性问题html
幂等是源于一种数学概念。其主要有两个定义数据库
若是在一元运算中,x 为某集合中的任意数,若是知足 f(x) = f(f(x)) ,那么该 f 运算具备幂等性,好比绝对值运算 abs(a) = abs(abs(a)) 就是幂等性函数。api
若是在二元运算中,x 为某集合中的任意数,若是知足 f(x,x) = x,前提是 f 运算的两个参数均为 x,那么咱们称 f 运算也有幂等性,好比求大值函数 max(x,x) = x 就是幂等性函数。安全
在数学中幂等的概念或许比较抽象,可是在开发中幂等性是极为重要的。简单来讲,对于同一个系统,在一样条件下,一次请求和重复屡次请求对资源的影响是一致的,就称该操做为幂等的。好比说若是有一个接口是幂等的,当传入相同条件时,其效果必须是相同的。服务器
特别是对于如今分布式系统下的 RPC 或者 Restful 接口互相调用的状况下,很容易出现因为网络错误等等各类缘由致使调用的时候出现异常而须要重试,这时候就必须保证接口的幂等性,不然重试的结果将与第一次调用的结果不一样,若是有个接口的调用链 A->B->C->D->E,在 D->E 这一步发生异常重试后返回了错误的结果,A,B,C也会受到影响,这将会是灾难性的。微信
在生活中常见的一些要求幂等性的例子:restful
- 博客系统同一个用户对同一个文章点赞,即便这人单身30年手速疯狂按点赞,那么实际上也只能给这个文章 +1 赞
- 在微信支付的时候,一笔订单应当只能扣一次钱,那么不管是网络问题或者bug等而从新付款,都只应该扣一次钱
在查阅网络资料的时候,我看到许多文章把幂等性和并发安全的问题有些混淆了。幂等性是系统接口对外的一种承诺,而不是实现,承诺屡次相同的操做的结果都会是同样的。而并发安全问题是当多个线程同时对同一个资源操做时,因为操做顺序等缘由致使结果不正确。网络
这两个其实是彻底独立的两个问题,好比说同一笔订单即便你不停的提交支付,若是扣除了屡次钱,就说明该操做不幂等。而有多笔订单同时进行支付,最后扣除金额不是这多笔金额的总和,那么说明该操做有并发安全问题。因此幂等性和并发安全是彻底两个维度的问题,要分开讨论解决。并发
我在一些讨论幂等性的文章中看到中给出的解决方案为‘悲观锁’和‘乐观锁’,这两个方案能够很好的解决并发问题,可是却不该该是幂等性问题的解决方案,特别是悲观锁是用于防止多个线程同时修改一个资源的。却是乐观锁的版本号机制能够勉强以 token
或者状态标识
做为版本号来实现幂等性(下文解释token
和状态标识
),勉强说的过去。分布式
因此说幂等性与并发安全是不一样的,在本文就只讨论幂等性的问题,对于并发安全问题不作讨论
若是把操做按照功能分类,那就是增删改查四种,在 http 协议中则表现为 Get、Post、Put、Delete 四种。
Get 方法用于获取资源,不该当对系统资源进行改变,因此是幂等的。注意这里的幂等提如今对系统资源的改变,而不是返回数据的结果,即便返回结果不相同可是该操做自己没有反作用,因此幂等。
Delete 方法用于删除资源,虽然改变了系统资源,可是第一次和第N次删除操做对系统的做用是相同的,因此是幂等的。好比要删除一个 id 为 1234 的资源,可能第一次调用时会删除,然后面全部调用的时候因为系统中已经没有这个 id 的资源了,可是第一次操做和后面的操做对系统的做用是相同的,因此这也是幂等的,调用者能够屡次调用这个接口没必要担忧错误。
修改操做有多是幂等的也可能不幂等。若是修改的资源为固定的,好比说把帐户中金额改成 1000 元,不管调用几回都是幂等的。假如资源不固定,好比帐户中金额减小50元,调用一次和调用屡次的结果确定不同,这时候就不幂等了。在修改操做中想要幂等在下文中讨论。
2019-08-13 修改原文对Put协议定义有错误,Put操做必须为幂等的,即若是声明为Put协议时就至关于对外声明这个接口是幂等的。因此对于原文举例说
帐户中金额减小50元
这种操做在Put协议中是不容许的,只能作相似于帐户中金额改成 1000 元
的操做
Post 新增操做天生就不是一个幂等操做,其在 http 协议的定义以下:
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.
在其定义中代表了 Post 请求用于建立新的资源,这意味着每次调用都会在系统中产生新的资源,因此该操做注定不是幂等操做。这时候想要幂等就必须在业务中实现,方案在下文会讨论。
在上面提到的幂等性仍是比较理论,下面结合一些常见的实际业务场景来讨论幂等性设计方案。
利用数据库的特性来实现幂等。一般是在表上构建一个惟一索引,那么只要某一个数据构建完毕,后面再次操做也没法成功写入。
常见的业务就是博客系统点赞功能,一个用户对一个博文点赞后,就把用户 id 与 博文 id 绑定,后续该用户点赞同一个博文就没法插入了。或是在金融系统中,给用户建立金融帐户,一个用户确定不能有多个帐户,就在帐户表中增长惟一索引来存储用户 id,这样即便重复操做用户也只能拥有一个帐户。
状态标识是很常见的幂等设计方式,主要思路就是经过状态标识的变动,保证业务中每一个流程只会在对应的状态下执行,若是标识已经进入下一个状态,这时候来了上一个状态的操做就不容许变动状态,保证了业务的幂等性。
状态标识常常用在业务流程较长,修改数据较多的场景里。最经典的例子就是订单系统,假如一个订单要经历 建立订单 -> 订单支付取消 -> 帐户计算 -> 通知商户 这四个步骤。那么就有可能一笔订单支付完成后去帐户里扣除对应的余额,消耗对应的优惠卷。可是因为网络等缘由返回了错误信息,这时候就会重试再次去进行帐户计算步骤形成数据错误。
因此为了保证整个订单流程的幂等性,能够在订单信息中增长一个状态标识,一旦完成了一个步骤就修改对应的状态标识。好比订单支付成功后,就把订单标识为修改成支付成功,如今再次调用订单支付或者取消接口,会先判断订单状态标识,若是是已经支付过或者取消订单,就不会再次支付了。
Token 机制应该是适用范围最普遍的一种幂等设计方案了,具体实现方式也不少样化。可是核心思想就是每次操做都生成一个惟一 Token 凭证,服务器经过这个惟一凭证保证一样的操做不会被执行两次。这个 Token 除了字面形式上的惟一字符串,也能够是多个标志的组合(好比上面提到的状态标志),甚至能够是时间段标识等等。
举个例子,在论坛中发布一个新帖子,这是一个典型的 Post 新增操做,要怎样防止用户屡次点击提交致使产生多个一样的帖子呢。可让用户提交的时候带一个惟一 Token,服务器只要判断该 Token 存在了就不容许提交,便能保证幂等性。
上面这个例子比较容易理解,可是业务比较简单。因为 Token 机制适用较广,因此其设计中要注意的要求也会根据业务不一样而不一样。
Token 在什么时候生成,怎么生成?这是该机制的核心,就拿上面论坛系统来讲,若是你在用户提交帖子的时候才生成 Token,那用户每次点提交都会生成新的 Token 而后都能提交成功,就不是幂等的了。必须在用户提交内容以前,好比进入编辑页面的时候生成 Token,用户在提交的时候内容带着 Token 一块儿提交,对于同一个页面不管用户提交多少次,就至多能成功一次。因此 Token 生成的时机必须保证可以使该操做具屡次执行都是相同的效果才行。使用 Token 机制就要求开发者对业务流程有较好的理解。
幂等性是开发当中很常见也很重要的一个需求。尤为是金融、支付等行业对其要求更加严格,既要有好的性能也要有严格的幂等性。除了对其概念的掌握,理解自身业务需求更是实现幂等功能的要点,必须处理好每个结点细节,一旦某个地方没有设计完善,最后的结果可能仍旧达不到要求。