接口冪等性介紹與應用
場景與問題
假設有兩個線程A和B,現假設線程A走到了開始事務和提交事務中間的流程,線程B還在判斷手機號是否存在的流程,因爲Mysql的默認事務隔離級別是repeatable-read,因此線程B不會讀取到線程A還未提交的數據。因此B線程判斷手機號是否存在的結果爲false,所以流程可以繼續往下走,又因爲主鍵是生成的UUID,不重複,所以出現了一個手機號註冊了兩個賬號的情況,直接導致登錄時的一段代碼邏輯出現異常,導致該用戶使用手機號無法登錄招聘系統。
出現該問題的原因是,從開始事務到提交事務的整個流程,不是原子性的,這個流程可以被分割,可以只完成部分。
接口冪等性
冪等性原本是數學上的概念,即使公式:f(x)=f(f(x)) 能夠成立的數學性質。用在編程領域,則意爲對同一個系統,使用同樣的條件,一次請求和重複的多次請求對系統資源的影響是一致的,在調用方多次調用的情況下,接口最終得到的結果是一致的。
除了查詢功能具有天然的冪等性之外,增加、更新、刪除都要保證冪等性。那麼如何來保證冪等性呢
接口冪等性場景
-
註冊賬號
註冊了賬號時,使用同一個手機號無論同時發送多少次請求,最終有且只有一個賬號能使用該手機號註冊成功。
-
申請職位
同一個人對同一個職位同時申請多次,最終只有一次申請能成功。
另外在支付和訂單的相關係統中,接口需要嚴格保證冪等性,否則可能出現訂單被支付兩次或者有兩份訂單的問題。
如何保證冪等性
唯一索引
防止新增髒數據。比如:招聘系統中的賬號,每個用戶只能有一個賬號,怎麼防止給用戶創建多個,那麼給賬戶表中的手機號丶郵箱丶身份證號等加唯一索引。
給數據庫表中唯一性的字段加上唯一索引,數據的唯一性由數據庫層面來保證。
優點:簡單方便,不需要額外代碼
缺點:索引的通用缺點,無法保證更新和刪除接口的冪等性
悲觀鎖
悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。
使用java synchronized關鍵字。導致系統變成單線程!
獲取數據的時候加鎖獲取。select * from table_xxx where
id=‘xxx’ for update;
注意:id字段一定是主鍵或者唯一索引,不然是鎖表;悲觀鎖使用時一般伴隨事務一起使用,數據鎖定時間可能會很長,根據實際情況選用;
缺點:降低系統併發能力
樂觀鎖
樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在提交更新的時候會判斷一下在此期間別人有沒有去更新這個數據。樂觀鎖適用於讀多寫少的應用場景,這樣可以提高吞吐量。
樂觀鎖只是在更新數據那一刻鎖表,其他時間不鎖表,所以相對於悲觀鎖,效率更高。樂觀鎖的實現方式多種多樣,可以通過version或者狀態條件等。
版本號控制
這種方法適合在更新的場景中,比如我們要更新一條記錄,這時我們就可以在更新的接口中增加一個版本號,來做冪等
先查詢當前版本號,select version from xxx where xxx
通過版本號實現update table_xxx set name=#name#,version=version+1 where version=#{version}
狀態機
這種方法適合在有狀態機流轉的情況下,比如訂單的創建和付款,訂單的創建肯定是在付款之前,這時我們可以通過在設計狀態字段時,使用int類型,並且通過值類型的大小來做冪等,比如訂單的創建爲0,付款成功爲100。付款失敗爲99。
在做狀態機更新時,我們就這可以這樣控制
update goods_order set status=#{status} where id=#{id} and status < #{status}
全局唯一id作爲分佈式鎖
如果使用全局唯一ID,就是根據業務的操作和內容生成一個全局ID,在執行操作前先根據這個全局唯一ID是否存在,來判斷這個操作是否已經執行。如果不存在則把全局ID,存儲到存儲系統中,比如數據庫、Redis等。如果存在則表示該方法已經執行。
使用redis實現分佈式鎖
在單節點下正確性較高,分佈式redis下極端情況會出現問題。
if(!stringRedisTemplate.hasKey("key")){
//todo 表示不存在鎖,可以加鎖
stringRedisTemplate.opsForValue().set("key", "value");
}else{
//todo 鎖已被其他線程佔有,不處理,返回
return new ApiResponse<>(-1, "請不要重複操作", null);
}
使用Redis加鎖方式1
if(stringRedisTemplate.opsForValue().setIfAbsent("key", "value", 60, TimeUnit.SECONDS)){
return new ApiResponse<>(-1, "請不要重複操作", null);
}
使用Redis加鎖方式2
redisTemplate的setIfAbsent實際上是調用了redis的set(key, value, ex, nx)指令,保證了命令的原子性,所以不會在高併發的情況下出現問題。
注意點
如果使用redis加鎖來保證冪等性,且業務流程使用了數據庫事務,那麼不可以使用Spring框架的自動事務控制,因爲@Transcational註解提交事務是在返回數據之後,在提交之前redis鎖以得到釋放,在釋放鎖和提交事務的間隙可能有其他線程執行命令從而引發錯誤。
redis的set命令是如何保證原子性的
對於Redis而言,命令的原子性指的是:一個操作的不可以再分,操作要麼執行,要麼不執行。Redis的操作之所以是原子性的,是因爲Redis是單線程的,所以在SET完成之前不會運行任何東西;這使得SET {key} {value} EX {expiry} NX非常適合簡單鎖定。
上面的鎖依然不是很完美,需要在此基礎上保證釋放的是自己線程鎖擁有的鎖。可通過讀取value來判斷是否是當前線程鎖擁有的鎖。
redis鎖的其他用途
可以作爲分佈式定時程序鎖(shedlock組件以提供實現,可直接調用api和使用註解來使用,相比於quartz,使用更方便,但不支持調度等功能,只能簡單的加鎖)
redis分佈式鎖之Redlock
分佈式redis集羣加鎖
多個master節點的redis鎖,在一半以上的節點上獲取鎖成功纔算最終獲取鎖成功。
優點:保證了高可用性。
缺點:實現複雜…
zookeeper作爲分佈式鎖
待研究。。。。
參考資料
Reclock官方文檔:https://redis.io/topics/distlock
分佈式後端接口冪等性設計:http://www.voidcn.com/article/p-uzgkvdjj-bqr.html
Mysql悲觀鎖與樂觀鎖:https://www.jianshu.com/p/f5ff017db62a