# Java冪等性 冪等:是一個數學概念,表示N次變換和1次變換的結果相同。 冪等操作:其特點是任意多次執行所產生的影響均與一次執行的影響相同(不會改變資源狀態,對數據沒有副作用)。 冪等性:一系列操作都是冪等操作。 冪等接口:冪等接口認爲,外部調用者會存在多次調用的場景,爲了防止重試對數據狀態的改變,需要將接口的設計爲冪等的 # 運用場景 1. 業務開發中,經常會遇到重複提交的情況,無論是由於網絡問題無法收到請求結果而重新發起請求。 2. 前端的操作抖動而造成重複提交情況。 3. 在交易系統,支付系統這種重複提交造成的問題有尤其明顯,比如: 用戶在APP上連續點擊了多次提交訂單,後臺應該只產生一個訂單。 4. 網絡抖動或者異常或者斷開導致不能正確返回。 5. 向支付寶發起支付請求,由於網絡問題或系統BUG重發,支付寶應該只扣一次錢。 很顯然,聲明冪等的服務認爲,外部調用者會存在多次調用的情況,爲了防止外部多次調用對系統數據狀態的發生多次改變,將服務設計成冪等。 # 冪等性方案 1. 查詢操作 查詢一次和查詢多次,在數據不變的情況下,查詢結果都是一樣的,select 是天然的冪等操作。 2. 刪除操作 刪除操作也是冪等的,刪除一次和刪除多次都是把數據刪除。 3. 建立唯一索引,防止新增臟數據 當表存在唯一索引,並發時新增重複記錄就會報錯,那麼這時候就查詢已存在的記錄並返回即可。 4. Token 機制,防止頁面重複提交 頁面數據只能夠提交一次,但是由於出現重複點擊或者網絡重發或 Nginx 重發等情況導致數據被重複提交的情況下,可以採用 Token+Redis(Redis 是單線程的,處理需要排隊)的解決方案。處理的流程是,在數據提交前要向服務器申請帶有有效時間的 Token,然後 Token 放到 Redis 或 JVM 內存中,當數據正式提交到後台要校驗 Token 並刪除 Token。 5. 悲觀鎖 獲取數據的時候加鎖獲取: ``` select * from table where id = 'xxx' for update; ``` >要注意的是,id 字段一定要是主鍵或者唯一索引,否則會導致鎖表。 悲觀鎖的使用一般伴隨事務一起使用,數據鎖定事件可能會很長,要根據實際情況慎用。 6. 樂觀鎖 樂觀鎖只是在更新數據的那一刻鎖表,其他時間不鎖表,所以相對於悲觀鎖效率更高。 樂觀鎖的實現方式多種多樣,可以通過 version 或者其他狀態條件。 7. 分佈式鎖 還是拿插入數據的例子,如果是分佈式系統,構建全局唯一索引比較困難,例如唯一性的字段無法確定。那麼這時候就可以引入分佈式鎖,通過第三方的系統(Redis 或 Zookeeper),在業務系統插入數據或更新數據,獲取分佈式鎖,然後做操作,之後再釋放鎖。這樣其實是把多線程並發鎖的思路引入了多個系統,也就是分佈式系統中的解決思路。 要注意的是,某個長流程處理過程要求不能並發執行,可以在流程執行之前根據某個標誌(用戶 ID + 後綴等)獲取分佈式鎖,其他流程執行時獲取鎖就會失敗,也就是同一時間該流程只能有一個能執行成功,執行完成後,釋放分佈式鎖(分佈式鎖需要第三方系統提供))。 8. select+insert 對於一些並發不高的後台系統,或者一些任務 Job,為了支持冪等,支持重複執行,簡單的處理方法是先查詢下一些關鍵數據,判斷是否已經執行過,然後再進行業務處理就可以了。但是要注意的是核心高並發流程不要用這種方法,因為效率較低。 9. 狀態機冪等 在設計單據相關的業務,或者是任務相關的業務,肯定會涉及到狀態機(狀態變更圖),就是業務單據上面有個狀態,狀態在不同的情況下會發生變更,一般情況下存在有限狀態機,這時候如果狀態機已經處於下一個狀態,卻來了一個上一個狀態的變更,理論上是不能夠變更的,這樣的話,保證了有限狀態機的冪等。 要注意的是,訂單等單據類業務,存在很長的狀態流轉,一定要深刻理解狀態機,對業務系統設計能力提高有很大幫助。 ## 冪等的不足 冪等是爲了簡化客戶端邏輯處理,卻增加了服務提供者的邏輯和成本,是否有必要,需要根據具體場景具體分析,因此除了業務上的特殊要求外,儘量不提供冪等的接口。 增加了額外控制冪等的業務邏輯,複雜化了業務功能; 把並行執行的功能改爲串行執行,降低了執行效率。 ## 策略 ### 保證冪等策略 * 冪等需要通過唯一的業務單號來保證: * 相同的業務單號,認為是同一業務 * 使用唯一的業務單號確保:後面多次相同業務單號的處理邏輯和執行效果是一致的 * 範例 - 支付流程: 1. 先查詢訂單是否支付過 2. 如果已經支付過,返回支付成功 3. 如果沒有支付,則進行支付流程,修改訂單的狀態為已支付 ### 防重複提交策略 * 在保證冪等的策略中,執行是分兩步執行的,後面一步依賴上面一步的查詢結果,這樣就無法保證原子性 * 無法保證原子性在高併發的情況下會存在問題: * 第二次請求在第一次請求的下一步訂單狀態沒有修改為"已支付狀態"時進行 * 為了解決這個問題 :將查詢和變更狀態操作加鎖,並將並行操作改為序列執行 ###### tags: `Java`