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