聊一聊幂等
幂等是什麼?百度上給出的解釋如下:
幂等(idempotent、idempotence)是一個數學與計算機學概念,常見于抽象代數中。在程式設計中一個幂等操作的特點是其任意多次執行所産生的影響均與一次執行的影響相同。幂等函數,或幂等方法,是指可以使用相同參數重複執行,并能獲得相同結果的函數。這些函數不會影響系統狀态,也不用擔心重複執行會對系統造成改變。例如,“setTrue()”函數就是一個幂等函數,無論多次執行,其結果都是一樣的.更複雜的操作幂等保證是利用唯一交易号(流水号)實作。
1
程式設計中的幂等
概念
在我們日常開發和業務實作中,對于相同的參數輸入,多次調用相同的功能,對資源的影響是一樣的,也就是一次和多次請求某一個資源應該具有同樣的副作用。
幂等解決的問題
- 表單重複送出;重複推送資料導緻多次更新後端資源導緻資料不一緻問題
- RPC逾時重試;服務被多次調用導緻資料不一緻問題
- SQL多次執行;程式問題導緻sql多次調用帶來資料不一緻問題
常見的幂等場景
我們在研發工作中,需要考慮幂等的場景也比較多,常見的就是接口幂等、消息幂等和資料庫幂等等。
2
接口幂等
接口幂等又常分為http接口幂等和RPC接口幂等。
HTTP接口幂等
我們常用的http接口對應的請求方式中:
- GET是幂等的,get方式是從伺服器端擷取資源,是單純的查詢操作,對服務端資源沒有更新,是以是幂等的。
- HEAD是幂等的,似于get請求,隻不過傳回的響應中沒有具體的内容,用于擷取報頭。
- POST一般是非幂等的,用于表單送出向服務端新增資料。
- PUT 一般也是幂等的,用于更新服務端資源。
- DELETE 一般也是幂等的,用于删除服務端資源。
其他請求方式我們平時基本很少用到,這裡不再一一列舉。
RPC接口幂等
RPC接口用于領域設計後的功能拆分,調用是跨程序的,對于RPC接口中的幂等,其實是對于外部調用逾時重試,或者同樣參數多次調用同一個接口,要保證對服務端資源的影響和一次調用是一樣的,我們經常遇到的情況是狀态機的變更,比如用戶端調用RPC服務完結退款的狀态,那麼多次調用要保證和一次調用一樣,退款狀态隻能被修改一次,并且最終的狀态是完結。
3
消息幂等
對于消息幂等,可能大家對其了解不是很多,我們應用中基本都會使用消息隊列,那麼肯定會涉及到消息投遞和消息接收,消息幂等也要分兩個次元來分析:
消息投遞幂等
所謂消息投遞幂等,就是同一條消息隻能被投遞一次,對于消息broker來說,就算同一條消息投遞多次,我也隻存儲一條。我們舉個例子來說明一下:
- ①消息發送者,嘗試發送一條消息到消息broker。
- ②消息broker收到消息後理論上要給一個響應結果給發送者,但是這個響應可能丢失了。
- ③對于②中的響應丢失或者沒有響應,消息發送者會認為沒有發送成功,重複投遞消息。
問題就在于響應丢失重複投遞,有可能消息broker已經成功接收消息并且存儲了,重複投遞的消息有可能被消息broker接收并存儲,導緻broker接收了兩條或者兩條以上的相同消息,也就會導緻消息接收方接收到多條相同的消息,在業務場景中可能造成業務異常。
對于這種重複投遞的消息,消息broker層可以對每一條消息生成一個唯一的code,有重複消息過來的時候,生成的code也會相同,如果發現相同就丢棄。
消息接收幂等
消息接收幂等,是消息broker中的同一條消息隻能被consumer接收處理一次,就算broker推送多次,也隻能消費一條。同樣舉個例子來說明一下:
- ①消息broker嘗試向consumer推送一條消息。
- ②消息consumer接收到消息後,向broker發送響應結果,但是丢失。
- ③消息broker由于沒有接收到②中的響應結果,認為consumer沒有收到消息,會重複推送。
消息broker重複推送相同消息的時候,有可能consumer已經接受成功并處理了業務,再次收到消息會導緻consumer重複處理邏輯,假如在資金相關的場景出現這種情況,會導緻重複出賬造成資損。
對于消息broker重複推送的消息,consumer要對每一條消息生成唯一id或者code,如果發現重複消息,直接丢棄。
4
資料庫幂等
所謂資料庫幂等,也就是我們對DB層的操作幂等,說人話就是保證我們業務操作sql是幂等的,其實就是我們同一條sql執行多次和執行一次的效果是一樣的,就拿CRUD來說,有些是幂等的,有些不是幂等但是可以通過調整轉換成幂等的:
查詢(Retrieve)
對于資料庫查詢,隻是單穿的從資料庫擷取資源,不會更新資料,是以是幂等的。
建立(Create)
對于新增資料操作,很多時候是非幂等的,執行一次insert就會新增一條資料(id是自增主鍵):
insert into User(id,name) values(null,'typhoon');
這條sql是非幂等的,每執行一次都會新增一條記錄,但是通過改造我們可以将其變成幂等的:
insert into User(id,name) values(100,'typhoon');
這樣就變成幂等的了,不使用自增序列,通過程式生成主鍵,這樣重複執行,資料庫也隻會新增一條資料。
更新(Update)
資料庫更新操作,有些是幂等的,有些是非幂等的,典型的場景就是值遞增,比如把小明的賬戶新增10000塊:
update Account set money = money + 10000 where name = '小明';
這條sql是非幂等的,如果由于上層程式有bug,導緻該sql重複被調用,那麼就趕緊找地方哭去吧。我們通過簡單的調整就可以把上述sql給造成幂等的:
var currentMoney = select money from Account where name = '小明';
var targetMoney = currentMoney + 10000;
update Account set money = targetMoney where name = '小明' and money = currentMoney;
這樣上述的非幂等sql就改造成了幂等了,有兩個關鍵的點需要注意:
- 使用"="代替"+",避免多次執行導緻多次增加。
- 增加條件money = currentMoney,避免并發的其他程式把金額扣了,導緻這條sql把扣掉的錢又加上了。
删除(Delete)
帶精确比對的删除sql是幂等的,其他的基本是非幂等的,典型的就是範圍删除:
delete from Table where id > 1000;
這個sql是非幂等的,在删除資料的同時,有并發程式在新增資料,那麼就導緻每一次執行delete都删除了資料,也就違反了執行多次和一次對資源的副作用一樣。經過改造把上述sql改成幂等的:
delete from Table where id in (?,?,?);
這樣把範圍删除變成了精确比對删除,也就變成了幂等sql。
總結
幂等對我們應用架構非常重要,大公司把幂等當做一個應用編碼規範,可見其重要程度,不管是網絡逾時重試、還是程式有bug導緻的多次調用,幂等在很大程度上保護了我們的系統資源。特别是牽扯到資金的場景,任何一層的實作都要做幂等。