最近跟朋友聊起這個話題,想深入了解下,於是學習總結,記錄下來,此文章參考以下博客綜合而來表示感謝:
參考:分布式系統接口冪等性
1. 接口調用存在的問題
現如今我們的系統大多拆分為分布式SOA,或者微服務,一套系統中包含了多個子系統服務,而一個子系統服務往往會去調用另一個服務,而服務調用服務無非就是使用RPC通信或者restful,既然是通信,那么就有可能在服務器處理完畢后返回結果的時候掛掉,這個時候用戶端發現很久沒有反應,那么就會多次點擊按鈕,這樣請求有多次,那么處理數據的結果是否要統一呢?那是肯定的!尤其在支付場景。
2. 什么是接口冪等性
接口冪等性就是用戶對於同一操作發起的一次請求或者多次請求的結果是一致的,不會因為多次點擊而產生了副作用。舉個最簡單的例子,那就是支付,用戶購買商品后支付,支付扣款成功,但是返回結果的時候網絡異常,此時錢已經扣了,用戶再次點擊按鈕,此時會進行第二次扣款,返回結果成功,用戶查詢余額返發現多扣錢了,流水記錄也變成了兩條...,這就沒有保證接口的冪等性
3. 什么情況下需要保證接口的冪等性
在增刪改查4個操作中,尤為注意就是增加或者修改,
A: 查詢操作
查詢對於結果是不會有改變的,查詢一次和查詢多次,在數據不變的情況下,查詢結果是一樣的。select是天然的冪等操作
B: 刪除操作
刪除一次和多次刪除都是把數據刪除。(注意可能返回結果不一樣,刪除的數據不存在,返回0,刪除的數據多條,返回結果多個,在不考慮返回結果的情況下,刪除操作也是具有冪等性的)
C: 更新操作
修改在大多場景下結果一樣,但是如果是增量修改是需要保證冪等性的,如下例子:
把表中id為XXX的記錄的A字段值設置為1,這種操作不管執行多少次都是冪等的
把表中id為XXX的記錄的A字段值增加1,這種操作就不是冪等的
D: 新增操作
增加在重復提交的場景下會出現冪等性問題,如以上的支付問題
4. 那么如何設計接口才能做到冪等呢?
常見的兩種實現方案: 1. 通過代碼邏輯判斷實現 2. 使用token機制實現 下面以支付系統為例,分別對接口的冪等性進行說明與實現
A: 通過代碼邏輯判斷實現接口冪等性,只能針對一些滿足判斷的邏輯實現,具有一定局限性
用戶購買商品的訂單系統與支付系統;訂單系統負責記錄用戶的購買記錄已經訂單的流轉狀態(orderStatus),支付系統用於付款,提供如下接口,訂單系統與支付系統通過分布式網絡交互。
boolean pay(int accountid,BigDecimal amount) //用於付款,扣除用戶的
這種情況下,支付系統已經扣款,但是訂單系統因為網絡原因,沒有獲取到確切的結果,因此訂單系統需要重試。由上圖可見,支付系統並沒有做到接口的冪等性,訂單系統第一次調用和第二次調用,用戶分別被扣了兩次錢,不符合冪等性原則(同一個訂單,無論是調用了多少次,用戶都只會扣款一次)。如果需要支持冪等性,付款接口需要修改為以下接口:
boolean pay(int orderId,int accountId,BigDecimal amount)
通過orderId來標定訂單的唯一性,付款系統只要檢測到訂單已經支付過,則第二次調用不會扣款而會直接返回結果:
在不同的業務中不同接口需要有不同的冪等性,特別是在分布式系統中,因為網絡原因而未能得到確定的結果,往往需要支持接口冪等性。
隨着分布式系統及微服務的普及,因為網絡原因而導致調用系統未能獲取到確切的結果從而導致重試,這就需要被調用系統具有冪等性。例如上文所闡述的支付系統,針對同一個訂單保證支付的冪等性,一旦訂單的支付狀態確定之后,以后的操作都會返回相同的結果,對用戶的扣款也只會有一次。這種接口的冪等性,簡化到數據層面的操作:
update userAmount set amount = 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是相同的,對系統同樣沒有產生修改。
B: 使用token機制實現接口冪等性,通用性強的實現方法
token機制實現步驟:
1. 生成全局唯一的token,token放到redis或jvm內存,token會在頁面跳轉時獲取.存放到pageScope中,支付請求提交先獲取token
2. 提交后后台校驗token,執行提交邏輯,提交成功同時刪除token,生成新的token更新redis ,這樣當第一次提交后token更新了,頁面再次提交攜帶的token是已刪除的token后台驗證會失敗不讓提交
token特點: 要申請,一次有效性,可以限流
注意: redis要用刪除操作來判斷token,刪除成功代表token校驗通過,如果用select+delete來校驗token,存在並發問題,不建議使用
參考:接口的冪等性