微服务架构---幂等机制
1背景介紹
1.1 冪等性定義
數學定義?
在數學里,冪等有兩種主要的定義:
- ? 在某二元運算下,冪等元素是指自己重復運算(或對于函數是為復合)的結果等于它自己的元素。例如,乘法下唯一兩個冪等實數為0和1,即s*s=s
- ? 某一元運算為冪等的時候,其作用在任一元素兩次后會和其作用一次的結果相同。例如,高斯符號便是冪等的,即f(f(x))=f(x)
HTTP規范定義?
在HTTP/1.1規范中冪等性的定義是:
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.
? ? HTTP的冪等性指的是一次和多次請求某一個資源應該具有相同的副作用。如通過PUT接口將數據的Status置為1,無論是第一次執行還是多次執行,獲取到的結果應該是相同的,即執行完成之后Status=1.
1.2 冪等概念
? ? 微服務架構中,冪等是一致性方面的一個重要概念。冪等(Idempotent)是一個數學領域與計算機學的概念,常見于抽象代數中。而在編程中,一個冪等操作的特定是指其任意多次執行所產生的影響均與一次執行的影響相同。
? ? 有人會簡單的認為,直接禁止所有重試即可。然而,重試是降低微服務失敗率的重要手段。因為,網絡波動、系統資源分配的不確定性、跨機房的請求等等原因,都會或多或少的導致一小部分請求的失敗。而這部分失敗的請求中,又有大部分請求其實只需要簡單重試幾次,即可成功。
1.3 重試機制
- 降低微服務失敗率
- 提高至四個或五個9
- 提高微服務架構的容錯性
- 提高微服務架構的高可靠性
2 冪等分析
2.1 冪等場景
? ? 可能會發生重復請求或消費的場景,在微服務架構中是隨處可見的。以下是筆者梳理的幾個常見場景:
- ?網絡波動: 因網絡波動,可能會引起重復請求
- 分布式消息消費: 任務發布后,使用分布式消息服務來進行消費
- 用戶重復操作: 用戶在使用產品時,可能會無意的觸發多筆交易,甚至沒有響應而有意觸發多筆交易
- 未關閉的重試機制:因開發人員、測試人員或運維人員沒有檢查出來,而開啟的重試機制(如Nginx重試、RPC通信重試或業務層重試等)
2.2 CRUD分析
- 新增類請求(C)
? ? ? ?數據庫自增主鍵,不具備冪等性
- 查詢類動作(R)
???????? ? ? ?重復查詢不會產生或變更新的數據,因此查詢是天然具備冪等性
- 更新類請求(U)
???????? ? ? ?基于主鍵的計算式Update,不具備冪等性,即: UPDATE goods SET number=number-1 WHERE id=1
? ? ? ?基于主鍵的非計算式Update,具備冪等性,即: UPDATE goods SET number=newNumber WHERE id=1
? ? ? ?基于條件查詢的Update,不一定具有冪等性(需要根據實際情況進行分析判斷)
- 刪除類請求(D)
???????? ? ? ?基于主鍵的Delete具備冪等性
? ? ? ?一般業務層面都是邏輯刪除(即Update操作),而基于主鍵的邏輯刪除操作也是具有冪等性的
2.3 冪等重要性
? ? 針對一個微服務架構,如果不支持冪等操作,那將會出現以下情況:
- ? ? 電商超賣現象
- ? ? 重復轉賬、扣款或付款
- ? ? 重復增加金幣、積分或優惠券
??超賣現象
? ? ? 比如某商品的庫存為1,此時用戶1和用戶2并發購買該商品,用戶1提交訂單后該商品的庫存被修改為0,而此時用戶2并不知道的情況下提交訂單,該商品的庫存再次被修改為-1,這就是超賣現象。
? ? ? 究其深層原因,是因為數據庫底層的寫操作和讀操作可以同時進行,雖然寫操作默認帶有隱式鎖(即對同一數據不能同時進行寫操作)但是讀操作默認是不帶鎖的,所以當用戶1去修改庫存的時候,用戶2依然可以讀到庫存Wie1,所以除夕拿了超賣現象。
? ? ??解決方案A:可以對讀操作加上顯示鎖(即在select ...語句最后加上for update)這樣一來用戶1在進行讀操作時用戶2就需要排隊等待了。但問題來了,如果該商品很熱門并發量很高那么效率就會大大的下降,如何解決呢?(解決方案B)
? ? ?解決方案B:我們可以有條件選擇的在讀操作上加鎖,比如可以對庫存做一個判斷,但庫存小于一個量時開始加鎖,讓排隊者排隊,這樣一來就解決了超賣現象。
3 何種接口提供冪等性
3.1 HTTP冪等性
? ? 在HTTP規范中定義GET、PUT和DELETE方法應該具有冪等性,具體如下:
- GET方法
? ??The GET method requests transfer of a current selected representatiofor the target resourceGET 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.
GET方法是向服務器查詢,不會對系統產生副作用,具有冪等性(不代表每次請求都是相同的結果)。
- PUT方法
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方法首先判斷系統中是否有相關的記錄,如果有記錄則更新該記錄,如果沒有則新增記錄。
- DELETE方法
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 方法是刪除服務器上的相關記錄。
3.2 實際業務案例
? ? 現在簡化為這樣一個系統,用戶購買商品的訂單系統與支付系統;訂單系統負責記錄用戶的購買記錄已經訂單的流轉狀態(orderStatus),支付系統用于付款,提供:
/*** 用于付款,扣除用戶的余額 **/ boolean pay(int accountid, BigDecimal amount);? ? 訂單系統與支付系統通過分布式網絡交互描述如下:
? 訂單冪等性
? ? 這種情況下,支付系統已經扣款,但是訂單系統因為網絡原因,沒有獲取到確切的結果,因此訂單系統需要重試。由上圖可見,支付系統并沒有做到接口的冪等性,訂單系統第一次調用和第二次調用,用戶分別被扣了兩次錢,不符合冪等性原則(同一個訂單,無論是調用了多少次,用戶都只會扣款一次)。如果需要支持冪等性,付款接口需要修改為以下接口:
boolean pay(int orderId, int accountId, BigDecimal amount);
通過orderId來標定訂單的唯一性,付款系統只要檢測到訂單已經支付過,則第二次調用不會扣款而會直接返回結果:
? ? 訂單支持冪等性
? ? ? ?在不同的業務中不同接口需要有不同的冪等性,特別是在分布式系統中,因為網絡原因而而未能得到確定的結果,往往需要支持接口冪等性。
3.3 分布式應用冪等性
? ? 隨著分布式應用及微服務的普及,因為網絡原因而導致調用應用未能獲取到確切的結果從而導致重試,這就需要被調用應用具有冪等性。例如上文所闡述的支付系統,針對同一個訂單保證支付的冪等性,一旦訂單的支付狀態確定之后,以后的操作都會返回相同的結果,對用戶的扣款也只會有一次。這種接口的冪等性,簡化到數據層面的操作:
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是相同的,對系統同樣沒有產生修改。
4 解決方案
4.1 全局唯一ID
? ? 如果使用全局唯一ID,就是根據業務的操作和內容生成一個全局ID,在執行操作前先根據這個全局唯一ID是否存在,來判斷這個操作是否已經執行。如果不存在則把全局ID,存儲到存儲系統中,比如數據庫、Redis等。如果存在則表示該方法已經執行。
? ? 使用全局唯一ID是一個通用方案,可以支持插入、更新、刪除業務操作。但是這個方案看起來很美但是實現起來比較麻煩,下面的方案適用于特定的場景,但是實現起來比較簡單。
4.2 去重表
? ? 這種方法適用于在業務中有唯一標的插入場景中,比如在以上的支付場景中,如果一個訂單只會支付一次,所以訂單ID可以作為唯一標識。這時,我們就可以建一張去重表,并且把唯一標識作為唯一索引,在我們實現時,把創建支付單據和寫入去重表,放在一個事務中,如果重復創建,數據庫會拋出唯一約束異常,操作就會回滾。
4.3 插入或更新
? ? 這種方法插入并且由唯一索引的情況,比如我們要關聯商品品類,其中商品的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()
4.4 多版本控制
? ? ? 這種方法適合在更新的場景中,比如我們要更新商品的名字,這時我們就可以在更新的接口中增加一個版本號,來做冪等:
boolean updateGoodsName(int id, String newName, int version);
? ? 在實現時可以如下:
update goods set name=#{newName}, version=#{version} where id=#{id} and version<${version}
4.5 狀態機控制
? ? 這種方法適合在有狀態機流轉的情況下,比如就會訂單的創建和付款,訂單的付款肯定是在之前,這時我們可以通過在設計狀態字段時,使用int類型,并且通過值類型的大小來做冪等,比如訂單的創建為0,付款成為為100,付款失敗為99.在做狀態機更新時,我們就可以這樣控制:
update goods_order set status=#{status} where id=#{id} and status<#{status}
? ? 以上就是保證冪等性的一些方法。
5 總結
? ? 冪等性設計不能脫離業務來討論,一般情況下,去重表同時也是業務數據表,而針對分布式的去重ID,可以參考以下幾種方式:
- UUID
- Snowflake
- 數據庫自增ID
- 業務本身的唯一約束
- 業務字段+時間戳拼接
?
?
轉自:https://my.oschina.net/yu120/blog/1790411
總結
以上是生活随笔為你收集整理的微服务架构---幂等机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微服务架构--链路追踪(Nginx篇)
- 下一篇: 【editor】Source Insig