在編程中,冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。冪等函數指的是那些使用相同參數重復執行也能獲得相同結果的函數。這些函數不會影響系統狀態,也不用擔心重復執行會對系統造成改變。比如說getIdCard()函數和setTrue()函數就是冪等函數。
冪等在我的理解里就是,一個操作不論被執行多少次,產生的效果和返回的結果都是一樣的。
一個冪等的操作典型如:把編號為5的記錄的A字段設置為0這種操作不管執行多少次都是冪等的。
一個非冪等的操作典型如:把編號為5的記錄的A字段增加1這種操作顯然就不是冪等的。
冪等的方案
1.查詢操作:Select是天然的冪等操作。
查詢一次和查詢多次,在數據不變的情況下,查詢的結果都是一樣的。
2.刪除操作:刪除操作也是冪等的,刪除一次和刪除多次都是把數據刪除。
因為刪除操作通常是定向的,比如通過id去刪除數據,如果該id在數據庫中存在對應記錄,則刪除該記錄;如果該id在數據庫中不存在對應記錄,也是執行的刪除記錄操作,只是沒有實質性地刪除到記錄而已,卻也不會有其他的副作用。
但是如果刪除操作具有返回值的話,可能返回的結果會不一樣,比如刪除一條記錄之后返回這條記錄中的某個值,如果刪除的數據不存在(已經在第一次的刪除請求中被刪除了),返回的就是空值了。
3.唯一索引:通過在數據庫表的一個字段上建立唯一索引可以有效防止新增臟數據。
比如有一個特殊訂單表,這個特殊訂單表關聯了一個用戶表,業務設置是每一個用戶只能創建一個特殊訂單,也就意味着在這個特殊訂單表中只能有一條用戶關聯的記錄。那么這時候就可以在這個特殊訂單表上針對這個用戶關聯的字段做一個唯一索引,通過數據庫的唯一約束來限制往特殊訂單表中插入多條一個用戶關聯的記錄。這樣,當第二次請求往特殊訂單表中插入一個用戶關聯的特殊訂單記錄的時候,數據庫就會報錯並回滾插入操作,也就保證了冪等。
4.Token校驗機制:操作前先校驗Token,以防止頁面重復提交。
原理上是通過Session Token來實現的,當然也可以通過Redis來實現。當客戶端請求頁面時,服務器會先生成一個全局唯一的Token,然后將該Token放置到Session或Redis當中,然后將Token發送給客戶端(一般通過構造Hidden表單或放在瀏覽器緩存中)。等下次客戶端提交請求時,Token就會隨着表單一起提交到服務器端。當服務器端第一次驗證通過之后,就會將Session中的Token值更新或刪除,若用戶重復提交,第二次的驗證判斷就是失敗,請求的操作也不會被重復執行。這是因為用戶提交的表單中的Token沒變,但服務器端的Session中的Token已經改變了或不存在了。
5.悲觀鎖:獲取數據的時候加鎖獲取。
select * from yanggb where id = 'huangq' for update;
悲觀鎖使用時一般伴隨事務一起使用,數據鎖定時間可能會很長,需要根據實際情況選用。
另外要注意的是,id字段一定是主鍵或者唯一索引,不然可能造成鎖表的結果,處理起來會非常麻煩。
6.樂觀鎖:通過版本號或其他狀態字段做更新限制。
與悲觀鎖長時間鎖表不一樣,樂觀鎖只是在更新數據那一刻鎖表,其他時間不鎖表。所以樂觀鎖相對於悲觀鎖,在大部分場景中效率會更高一些。樂觀鎖的實現方式多種多樣,可以通過version或者其他狀態條件。
id | name | age | version |
1 | yanggb | 18 | 1 |
比如給業務表內添加一個版本號的字段,如果要調用一個接口去更新年齡之前,就需要先查一下他的版本號是多少,然后調用接口的時候帶上版本號。
在接口里保證分布式接口的冪等性(在更新的SQL中添加version的條件判斷):
update user set age = 21, version = version + 1 where id = 1 and version = 1;
這樣,多次提交的請求,因為版本號(version)都一樣,因為第一次請求執行成功之后version已經+1了,則后面的請求因為version對應不上,都不會被執行。
7.分布式鎖:另一個角度的Token校驗。
如果是分布式系統的話,構建全局唯一索引會比較困難,比如唯一性的字段就沒有辦法確定。這時候可以引入分布式鎖,通過第三方的系統(Redis或Zookeeper),在業務系統插入數據或者更新數據前,需要先獲取分布式鎖,然后才能做操作,操作完成之后就釋放鎖。這樣其實是把單機系統里面多線程並發鎖的思路引入了多個系統的場景,也就是分布式系統中的解決思路。
要點:某個長流程處理過程要求不能並發執行,可以在流程執行之前根據某個標志(用戶ID+后綴等)獲取分布式鎖,其他流程執行時獲取鎖就會失敗,也就是同一時間該流程只能有一個能執行成功,執行完成后,釋放分布式鎖(分布式鎖要第三方系統提供)。
8.select + insert:一種簡單卻比較笨的方式。
對於一些並發不高的后台系統,或者一些任務JOB,為了支持冪等,支持重復執行,可以采取的一種簡單處理方法是,先根據一些關鍵數據到表中查詢記錄,以此來判斷是否已經執行過,判斷后再進行業務處理就可以了。
要注意的是,核心高並發流程不要用這種方法,因為要查詢一遍數據(你想想為什么要會把數據放到Redis中),性能太低了。
9.狀態機冪等:另一個角度的樂觀鎖。
在設計單據相關的業務,或者是任務相關的業務,肯定會涉及到狀態機(狀態變更圖)。簡單理解,就是業務單據上面有個狀態的字段,狀態在不同的情況下會發生變更,一般情況下存在有限狀態機。這時候,如果狀態機已經處於下一個狀態,這時候來了一個上一個狀態的變更,理論上是不能夠變更的,這樣的話,保證了有限狀態機的冪等。注意:訂單等單據類業務,存在很長的狀態流轉,一定要深刻理解狀態機,對業務系統設計能力提高有很大幫助。
總結
冪等的概念與分布式、高並發或JavaEE的概念都沒有關系,其只關心操作被多次執行產生的影響是否與一次執行是一致的。
事實上,要做到冪等性,只要從接口的設計上出發,不設計出任何非冪等的操作即可。譬如說有一個需求是,當用戶點擊贊同時,將答案的贊同數量+1。直接修改用戶點贊表(答案id,點贊數)的點贊數(+1)顯然不是冪等的。這種場景就可以改為:當用戶點擊贊同時,往用戶點贊表(答案id,用戶id)中添加一條記錄,然后通過在用戶id字段上建立唯一索引來確保在答案贊同表中只存在一條一個用戶點贊的記錄,最后的贊同數量由答案贊同表通過count去統計出來。當然了,實際的系統也不會這么設計,這里只是我沒有想到更好的例子。說起來簡單,做起來真的太難了,哈哈哈。
總之冪等性應該是合格程序員的一個基因,在設計系統時,是首要考慮的問題,尤其是在像支付寶,銀行或互聯網金融公司等涉及的都是金錢錢的系統,既要高效,也要保證數據准確,不能出現多扣款,多打款等問題,這樣會很難處理,用戶體驗也不好。
"他們就像星星一樣那么亮,我永遠都夠不着。"