1 背景
2 冪等性概念
冪等(idempotent)是一個數學與計算機學概念,常見於抽象代數中。
在我們的開發過程中,保證冪等性就是保證你的程式的無論執行多少次,影響均與第一次執行的影響是一致的,產生的結果也是一樣的。
而冪等函式(冪等方法),是指使用相同的引數結構重複執行,產生相同的結果的函式,重複執行冪等函式不會影響系統的狀態或者造成改變。
例如,"getUserName(String uCode)" 和 "delUser(String uCode)" 函式就是典型的冪等函式,而更復雜的冪等保證是類似 高併發場景下的訂單號(流水號)或者 秒殺場景下的唯一有效資料 等。
所以,冪等就是一個操作,不論執行多少次,產生的效果和返回的結果都是一樣的。
3 冪等性問題的常見解決方案
3.1 查詢操作和刪除操作
1 -- 使用者庫查詢某個身份證號的使用者名稱 2 select user_name from t_user where id_no ='xxx'; 3 4 -- 使用者庫刪除某個身份證號的使用者 5 delete from t_user where id_no ='xxx';
3.2 使用唯一索引 或者唯一組合索引
1 CREATE UNIQUE INDEX uni_user_userid ON t_user(userid);
3.3 token機制
防止頁面重複提交而導致的資料重複
業務現象: 頁面的資料只能被提交一次,或者提交多次的結果是一致的,不會產生多餘的髒資料。
產生的原因: 由於系統卡頓導致的重複點選或網路重發,還有就是nginx重發等情況,導致的資料被重複提交;
解決方法:
- 叢集環境採用token加redis(redis單執行緒的,處理需要排隊);
- 單JVM環境:採用token加redis或token加jvm記憶體。
處理步驟:
- 資料提交前要向服務的申請token,token放到redis或jvm記憶體,token需要設定有效時間,一般我們一個請求從request到respond時間是很短的,所以有效時間可以設定短一點;
- 提交後後臺校驗token,同時刪除token,返回執行結果。token特點:一次有效性,用完即刪,可以限流執行。
3.4 悲觀鎖
獲取資料的時候加鎖獲取。 select * from t_name where id='xxx' for update;
注意:這邊的id欄位一定是主鍵或者唯一索引,不然會導致鎖表。悲觀鎖使用時一般會配合事務一起使用,資料鎖定時間可能會很長,根據實際情況選用。
3.5 樂觀鎖
樂觀鎖只是在更新資料那一刻鎖表,其他時間不鎖表,所以相對於悲觀鎖,效率更高,適用於多讀少寫的型別,併發大的情況。
樂觀鎖的實現方式多種多樣,可以通過version或者其他狀態條件:
1. 通過版本號實現 update t_name set name=#{name},version=version+1 where version=#{version};
2. 通過條件限制 update t_name set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0
使用版本號的方式執行過程如下圖:
1 update t_name set name=#name#,version=version+1 where id=#id# and version=#version#; 2 update t_name set avai_amount=avai_amount-#subAmount# where id=#id# and avai_amount-#subAmount# >= 0;
3.6 分散式鎖
如果是分佈是系統,構建全域性唯一索引比較困難,不同的鏈路業務可能分佈在不同的資料庫表中,所以唯一性的欄位沒法確定,這時候可以引入分散式鎖,通過第三方的系統(redis或zookeeper),
在業務系統插入資料或者更新資料,獲取分散式鎖,然後做操作,完成業務操作之後,釋放鎖,這樣其實是把多執行緒併發的鎖的思路,引入多多個系統,也就是分散式系統中得解決思路。
關鍵點:某個長流程處理過程要求不能併發執行,可以在流程執行之前根據某個標誌(使用者ID+字尾等)獲取分散式鎖,其他流程執行時獲取鎖就會失敗,也就是同一時間該流程只能有一個能執行成功,執行完成後,釋放分散式鎖(分散式鎖要第三方系統提供)。
後面有一篇專門分析分散式鎖方案的文章,還在草稿箱,待整理。
3.7 select + insert
併發不高的後臺系統,或者一些簡單的執行任務,為了支援冪等,支援重複執行,簡單的處理方法是,先查詢下一些關鍵資料,判斷是否已經執行過,在進行業務處理,就可以了。
但是同樣有問題,核心高併發流程不便使用這種方法。因為他本質上還是兩個步驟,中間還有執行間隙的,在超高併發的情況還是會造成資料不一致的情況,這對於核心業務就是災難了。
3.8 狀態機冪等
3.9 保證Api介面的冪等性
如銀聯提供的付款介面:需要接入商戶提交付款請求時附帶:source來源,seq序列號 ,source+seq在資料庫裡面做唯一索引,防止多次付款(併發時,只能處理一個請求) 。
關鍵點:核心業務功能,對外提供介面為了支援冪等呼叫,介面有兩個欄位必須傳,一個是來源source,一個是來源方序列號seq,這個兩個欄位在提供方系統裡面做聯合唯一索引,這樣當第三方呼叫時,
先在本方系統裡面查詢一下,是否已經處理過,返回相應處理結果;沒有處理過,進行相應處理,返回結果。為了冪等友好,最好先查詢一下,是否處理過該筆業務,不查詢直接插入業務系統,會報錯,而實際是已經處理過了。
4 會議室的解決方案
將每天的會議預定按照半個小時1位做48位佔用位符預算,建立快取機制,進行高效率的佔位判斷,並反寫到預定表;啟動額外排程服務做最終的預定持久化;
採用唯一聯合索引保障高併發下的冪等性策略。將會議室ID、時間段、日期,建立唯一組合索引,防止新增髒資料,保證不會有兩條一樣的會議室預定記錄插入
1 CREATE UNIQUE CLUSTERED INDEX [ClusteredIndex_A9_MeetingReser] ON A9_MeetingReser 2 ( 3 [timespan] ASC, 4 [roomid] ASC, 5 [sdate] ASC 6 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
執行會議預訂的事務指令碼,如下,當資料庫中存在一樣的會議室資訊時,會返回錯誤(被佔用)的狀態值。
1 BEGIN TRAN T_Add; 2 DECLARE @code INT; DECLARE @occupyMeeing TABLE ( sMeetCode INT ); 3 DECLARE @resutlTable TABLE ( lType TINYINT,/*返回型別0為失敗型別,1為成功型別*/ resutlValue NVARCHAR(60)/*返回的資訊*/ ); 4 -- Todo 業務邏輯 寫入資料庫操作,即會議號和佔用的時間段標識為聯合索引,不可重複插入,重複插入報錯 5 IF @@ERROR!=0 goto w_err; 6 COMMIT TRAN T_Add ; 7 goto w_end w_err: 8 ROLLBACK TRAN T_Add ; 9 w_end: SELECT * FROM @resutlTable;
原來從預定到判斷佔用到寫庫會耗時0.5~1s,優化後整個流程執行效能提升到50ms左右,避免了會議室預定衝突的情況。
結果:根據會議室預定記錄的統計,優化釋出之後再未發生過預定衝突的問題。免除了會議管理員與預定人員溝通協調會議室的成本,解決了長期困擾他們的問題。
5 總結
冪等本質上與系統是否分散式、高併發,業務執行頻率高不高,沒有直接的關係。關鍵是程式的操作過程是不是冪等的。
典型的冪等操作就是:把某個變數設定為1這種行為,不管執行多少次都是冪等的,你在進行網際網路支付的時候,即使系統卡頓,你提交多次,也只支付一次。
要做到冪等性,從介面設計上來說不設計任何非冪等的操作即可。特別在類似支付寶,銀行,網際網路金融公司等涉及的網上資金系統,既要高效,資料也要準確,不能出現多扣款,多打款,產生金錢交易不一致等問題。