在數學裏,冪等有兩種主要的定義:- 在某二元運算下,冪等元素是指被本身重複運算(或對於函數是爲複合)的結果等於它本身的元素。例如,乘法下惟一兩個冪等實數爲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