什麼是冪等性

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.算法

這裏須要關注幾個重點:數據庫

  1. 冪等不只僅只是一次(或屢次)請求對資源沒有反作用(好比查詢數據庫操做,沒有增刪改,所以沒有對數據庫有任何影響)。緩存

  2. 冪等還包括第一次請求的時候對資源產生了反作用,可是之後的屢次請求都不會再對資源產生反作用。網絡

  3. 冪等關注的是之後的屢次請求是否對資源產生的反作用,而不關注結果。多線程

  4. 網絡超時等問題,不是冪等的討論範圍。併發

冪等性是系統服務對外一種承諾(而不是實現),承諾只要調用接口成功,外部屢次調用對系統的影響是一致的。聲明爲冪等的服務會認爲外部調用失敗是常態,而且失敗以後必然會有重試。異步

什麼狀況下須要冪等

業務開發中,常常會遇到重複提交的狀況,不管是因爲網絡問題沒法收到請求結果而從新發起請求,或是前端的操做抖動而形成重複提交狀況。 在交易系統,支付系統這種重複提交形成的問題有尤爲明顯,好比:分佈式

  1. 用戶在APP上連續點擊了屢次提交訂單,後臺應該只產生一個訂單;ide

  2. 向支付寶發起支付請求,因爲網絡問題或系統BUG重發,支付寶應該只扣一次錢。 很顯然,聲明冪等的服務認爲,外部調用者會存在屢次調用的狀況,爲了防止外部屢次調用對系統數據狀態的發生屢次改變,將服務設計成冪等。

冪等VS防重

上面例子中小明遇到的問題,只是重複提交的狀況,和服務冪等的初衷是不一樣的。重複提交是在第一次請求已經成功的狀況下,人爲的進行屢次操做,致使不知足冪等要求的服務屢次改變狀態。而冪等更多使用的狀況是第一次請求不知道結果(好比超時)或者失敗的異常狀況下,發起屢次請求,目的是屢次確認第一次請求成功,卻不會因屢次請求而出現屢次的狀態變化。

什麼狀況下須要保證冪等性

以SQL爲例,有下面三種場景,只有第三種場景須要開發人員使用其餘策略保證冪等性:

  1. SELECT col1 FROM tab1 WHER col2=2,不管執行多少次都不會改變狀態,是自然的冪等。

  2. UPDATE tab1 SET col1=1 WHERE col2=2,不管執行成功多少次狀態都是一致的,所以也是冪等操做。

  3. UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次執行的結果都會發生變化,這種不是冪等的。

爲何要設計冪等性的服務

冪等可使得客戶端邏輯處理變得簡單,可是卻以服務邏輯變得複雜爲代價。知足冪等服務的須要在邏輯中至少包含兩點:

  1. 首先去查詢上一次的執行狀態,若是沒有則認爲是第一次請求

  2. 在服務改變狀態的業務邏輯前,保證防重複提交的邏輯

冪等的不足

冪等是爲了簡化客戶端邏輯處理,卻增長了服務提供者的邏輯和成本,是否有必要,須要根據具體場景具體分析,所以除了業務上的特殊要求外,儘可能不提供冪等的接口。

  1. 增長了額外控制冪等的業務邏輯,複雜化了業務功能;

  2. 把並行執行的功能改成串行執行,下降了執行效率。

保證冪等策略

冪等須要經過惟一的業務單號來保證。也就是說相同的業務單號,認爲是同一筆業務。使用這個惟一的業務單號來確保,後面屢次的相同的業務單號的處理邏輯和執行效果是一致的。 下面以支付爲例,在不考慮併發的狀況下,實現冪等很簡單:①先查詢一下訂單是否已經支付過,②若是已經支付過,則返回支付成功;若是沒有支付,進行支付流程,修改訂單狀態爲‘已支付’。

防重複提交策略

上述的保證冪等方案是分紅兩步的,第②步依賴第①步的查詢結果,沒法保證原子性的。在高併發下就會出現下面的狀況:第二次請求在第一次請求第②步訂單狀態尚未修改成‘已支付狀態’的狀況下到來。既然得出了這個結論,餘下的問題也就變得簡單:把查詢和變動狀態操做加鎖,將並行操做改成串行操做。

樂觀鎖

若是隻是更新已有的數據,沒有必要對業務進行加鎖,設計表結構時使用樂觀鎖,通常經過version來作樂觀鎖,這樣既能保證執行效率,又能保證冪等。例如: UPDATE tab1 SET col1=1,version=version+1 WHERE version=#version# 不過,樂觀鎖存在失效的狀況,就是常說的ABA問題,不過若是version版本一直是自增的就不會出現ABA的狀況。(從網上找了一張圖片很能說明樂觀鎖,引用過來,出自Mybatis對樂觀鎖的支持) 

防重表

使用訂單號orderNo作爲去重表的惟一索引,每次請求都根據訂單號向去重表中插入一條數據。第一次請求查詢訂單支付狀態,固然訂單沒有支付,進行支付操做,不管成功與否,執行完後更新訂單狀態爲成功或失敗,刪除去重表中的數據。後續的訂單由於表中惟一索引而插入失敗,則返回操做失敗,直到第一次的請求完成(成功或失敗)。能夠看出防重表做用是加鎖的功能。

分佈式鎖

這裏使用的防重表可使用分佈式鎖代替,好比Redis。訂單發起支付請求,支付系統會去Redis緩存中查詢是否存在該訂單號的Key,若是不存在,則向Redis增長Key爲訂單號。查詢訂單支付已經支付,若是沒有則進行支付,支付完成後刪除該訂單號的Key。經過Redis作到了分佈式鎖,只有此次訂單訂單支付請求完成,下次請求才能進來。相比去重表,將放併發作到了緩存中,較爲高效。思路相同,同一時間只能完成一次支付請求。 

token令牌

這種方式分紅兩個階段:申請token階段和支付階段。 第一階段,在進入到提交訂單頁面以前,須要訂單系統根據用戶信息向支付系統發起一次申請token的請求,支付系統將token保存到Redis緩存中,爲第二階段支付使用。 第二階段,訂單系統拿着申請到的token發起支付請求,支付系統會檢查Redis中是否存在該token,若是存在,表示第一次發起支付請求,刪除緩存中token後開始支付邏輯處理;若是緩存中不存在,表示非法請求。 實際上這裏的token是一個信物,支付系統根據token確認,你是你媽的孩子。不足是須要系統間交互兩次,流程較上述方法複雜。 

支付緩衝區

把訂單的支付請求都快速地接下來,一個快速接單的緩衝管道。後續使用異步任務處理管道中的數據,過濾掉重複的待支付訂單。優勢是同步轉異步,高吞吐。不足是不能及時地返回支付結果,須要後續監聽支付結果的異步返回。

CAS(樂觀鎖)以及ABA問題

獨佔鎖是一種悲觀鎖,synchronized就是一種獨佔鎖;它假設最壞的狀況,而且只有在確保其它線程不會形成干擾的狀況下執行,會致使其它全部須要鎖的線程掛起直到持有鎖的線程釋放鎖。

所謂樂觀鎖就是每次不加鎖,假設沒有衝突而去完成某項操做;若是發生衝突了那就去重試,直到成功爲止。

CAS(Compare And Swap)是一種有名的無鎖算法。CAS算法是樂觀鎖的一種實現。CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B並返回true,不然返回false。

注:synchronized和ReentrantLock都是悲觀鎖。

注:何時使用悲觀鎖效率更高、什麼使用使用樂觀鎖效率更高,要根據實際狀況來判斷選擇。

什麼是CAS機制
CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較並替換。

CAS機制當中使用了3個基本操做數:內存地址V,舊的預期值A,要修改的新值B。

更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改成B。

CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較並替換。

CAS機制當中使用了3個基本操做數:內存地址V,舊的預期值A,要修改的新值B。

更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改成B。

這樣說或許有些抽象,咱們來看一個例子:

1.在內存地址V當中,存儲着值爲10的變量。

 

2.此時線程1想要把變量的值增長1。對線程1來講,舊的預期值A=10,要修改的新值B=11。

 

3.在線程1要提交更新以前,另外一個線程2搶先一步,把內存地址V中的變量值率先更新成了11。

 

4.線程1開始提交更新,首先進行A和地址V的實際值比較(Compare),發現A不等於V的實際值,提交失敗。

 

5.線程1從新獲取內存地址V的當前值,並從新計算想要修改的新值。此時對線程1來講,A=11,B=12。這個從新嘗試的過程被稱爲自旋。

 

6.這一次比較幸運,沒有其餘線程改變地址V的值。線程1進行Compare,發現A和地址V的實際值是相等的。

 

7.線程1進行SWAP,把地址V的值替換爲B,也就是12。

 

從思想上來講,Synchronized屬於悲觀鎖,悲觀地認爲程序中的併發狀況嚴重,因此嚴防死守。CAS屬於樂觀鎖,樂觀地認爲程序中的併發狀況不那麼嚴重,因此讓線程不斷去嘗試更新。

CAS的優缺點:
樂觀鎖避免了悲觀鎖獨佔對象的現象,同時也提升了併發性能,樂觀鎖是對悲觀鎖的改進,雖然它也有缺點,但它確實已經成爲提升併發性能的主要手段,並且jdk中的併發包也大量使用基於CAS的樂觀鎖。但它也有缺點,以下:

1.CPU可能開銷較大
在併發量比較高的狀況下,若是許多線程反覆嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很大的壓力。

2.不能保證代碼塊的原子性
CAS機制所保證的只是一個變量的原子性操做,而不能保證整個代碼塊的原子性。好比須要保證3個變量共同進行原子性的更新,就不得不使用悲觀鎖了。

3.ABA問題。
CAS的核心思想是經過比對內存值與預期值是否同樣而判斷內存值是否被改過,但這個判斷邏輯不嚴謹,假如內存值原來是A,後來被一條線程改成B,最後又被改爲了A,則CAS認爲此內存值並無發生改變,但其實是有被其餘線程改過的,這種狀況對依賴過程值的情景的運算結果影響很大。解決的思路是引入版本號,每次變量更新都把版本號加一。

ABA問題:
線程1準備用CAS修改變量值A,在此以前,其它線程將變量的值由A替換爲B,又由B替換爲A,而後線程1執行CAS時發現變量的值仍然爲A,因此CAS成功。但實際上這時的現場已經和最初不一樣了。

 

 

ABA問題處理:
思路:解決ABA最簡單的方案就是給值加一個修改版本號,每次值變化,都會修改它版本號,CAS操做時都對比此版本號。

 

JAVA中ABA中解決方案(AtomicStampedReference/AtomicMarkableReference)

AtomicStampedReference 本質是有一個int 值做爲版本號,每次更改前先取到這個int值的版本號,等到修改的時候,比較當前版本號與當前線程持有的版本號是否一致,若是一致,則進行修改,並將版本號+1(固然加多少或減多少都是能夠本身定義的),在zookeeper中保持數據的一致性也是用的這種方式;

AtomicMarkableReference則是將一個boolean值做是否有更改的標記,本質就是它的版本號只有兩個,true和false,修改的時候在這兩個版本號之間來回切換,這樣作並不能解決ABA的問題,只是會下降ABA問題發生的概率而已;

相關文章
相關標籤/搜索