作者:小牛呼嚕嚕 | https://xiaoniuhululu.com
計算機內(nèi)功、源碼解析、科技故事、項目實戰(zhàn)、面試八股等更多硬核文章,首發(fā)于公眾號「小牛呼嚕?!?/p>
- 什么是冪等性?
- 為什么需要保證冪等性?
- 接口冪等設計和防止重復提交可以等同嗎?
-
常用保證冪等性的措施
- 先select再insert
- 數(shù)據(jù)庫設置唯一索引或唯一組合索引
- 去重表
- insert中加入exist條件判斷
- 悲觀鎖
- 樂觀鎖
- 狀態(tài)機
- 分布式鎖
- token機制
- 尾語
什么是冪等性?
大家好,我是呼嚕嚕,所謂冪等性就是:任意次數(shù)請求 同一個資源,對資源的狀態(tài)產(chǎn)生的影響和執(zhí)行一次請求是相同的。
比如對于接口來說,無論調(diào)用多少次同一個接口,對資源的狀態(tài)都只產(chǎn)生一次影響
為什么需要保證冪等性?
為什么需要做接口的冪等性?如果不做會發(fā)生什么事情?我們在實際企業(yè)開發(fā)過程中,如果僅是對數(shù)據(jù)庫進行查詢、刪除指定記錄操作,重復提交是沒啥問題的。但是如果是新增或者修改操作,就需要考慮重復提交的問題。
比如,如果一個訂單支付的時候,因各種原因重復提交多次,那如果沒有冪等性處理的話,這個訂單將會被支付多次的錢,這種和錢有關的錯誤是絕對不能容忍的。
經(jīng)常發(fā)生重復提交的場景:
- 當我們在公司的系統(tǒng)里面,提交表格,前端沒有對保存按鈕的做控制,可以多次點擊,然后我們又不小心快速點了多次,或者是網(wǎng)絡卡頓, 還是其他原因,以為沒有成功提交,就一直點擊保存按鈕,這樣都會產(chǎn)生重復提交表單請求。
- 在實際開發(fā)過程中,網(wǎng)絡波動是常有的事,所以很多時候 HTTP 客戶端工具都默認開啟超時重試的機制,這樣就無法避免產(chǎn)生重復的請求。
- 還有就是項目可能使用一些中間件,比如kafka消費生產(chǎn)者產(chǎn)生的消息時,可能讀到重復的消息,這樣也會產(chǎn)生重復的請求。
- ......
接口冪等設計和防止重復提交可以等同嗎?
接口冪等和防止重復提交有交集,但是嚴格來說并不完全等同
- 防重設計,主要從客戶端/前端的角度來解決,主要為了避免重復提交,對每次請求的返回結(jié)果無限制,前端常見的手段:點擊提交按鈕變灰、點擊后跳轉(zhuǎn)結(jié)果頁、每次頁面初始化生成隨機碼,提交時隨機碼緩存,后續(xù)重復的隨機碼請求直接不提交
- 冪等設計,強調(diào)更多地是當重復提交請求無法避免的時候,還能保證每次請求都返回一樣的結(jié)果。像我們上面對前端做的限制,是能繞過去的,抓包是能直接把接口給抓出來的,比如惡意批量調(diào)用接口,所以企業(yè)級系統(tǒng),前后端都需要做限制,特別是涉及到錢的業(yè)務。絕不能偷懶,后面我們來詳細講講對接口冪等的限制。
常用保證冪等性的措施
先select再insert
新手小白,在往數(shù)據(jù)庫插入數(shù)據(jù)時,為了防止重復插入,一般會在insert前,通過關鍵字去先select一下,如果查不到記錄就執(zhí)行insert操作,否則就不插入
但如果并發(fā)場景下,這個就不行了。比如線程2,在線程1插入數(shù)據(jù)前,執(zhí)行select,最終它也會去執(zhí)行插入操作,這樣就會產(chǎn)生2條記錄。所以在實際開發(fā)過程中,是不建議如此操作的。
數(shù)據(jù)庫設置唯一索引或唯一組合索引
數(shù)據(jù)庫設置唯一索引是我們最常用的方式,一個非常簡單,并且有效的方案。當記錄多次插入數(shù)據(jù)庫,會由于Id或者關鍵字段索引唯一的限制,導致后續(xù)記錄插入失敗
--創(chuàng)建唯一索引
alter table `order` add UNIQUE KEY `索引名` (`字段`);
第一條記錄插入到數(shù)據(jù)庫中,當后面其他相同的請求,再插入時,數(shù)據(jù)庫會報異常Duplicate entry 'xx' for key 'xx_name'
,這個異常不會對數(shù)據(jù)庫中既有的數(shù)據(jù)有影響,我們只需對異常進行捕獲就行,直接返回,代表已經(jīng)執(zhí)行過當前請求。
筆者這里介紹一個騷操作:INSERT IGNORE
insert ignore INTO tableName VALUES ("id","xxx")
咦,會有讀者覺得,這樣哪怕索引沖突了,數(shù)據(jù)庫會忽略錯誤返回影響行數(shù)0,這樣就不用再在代碼中,手動捕捉異常了,又方便又省事!
但事實真這樣嗎???
如果希望在每次插入新記錄時,自動地創(chuàng)建主鍵字段的值。一般會將主鍵id的屬性設為AUTO_INCREMENT
,
如果我們使用INSERT IGNORE
時,沒有成功新增記錄,但是AUTO_INCREMENT
會自動+1
,binlog中也沒有 INSERT IGNORE
語句日志。這個會導致主從數(shù)據(jù)一致性問題,如果線上環(huán)境數(shù)據(jù)庫是主從架構(gòu),從庫該字段的AUTO_INCREMENT
值會和主庫不一致,切庫(從庫變成總庫)的時候會沖突。
當然,查詢Mysql官方手冊,發(fā)現(xiàn)innodb_autoinc_lock_mode
用于平衡性能與主從數(shù)據(jù)一致性,令 innodb_autoinc_lock_mode=0可以解決這個問題,將其設為0后, 所有的insert語句都要在語句開始的時候得到一個表級的auto_inc鎖,在語句結(jié)束的時候才釋放這把鎖。也就是說在INSERT未成功執(zhí)行時AUTO_INCREMENT
不會自增,但是其也有缺點,會影響到數(shù)據(jù)庫的并發(fā)插入性能。
Mysql官方手冊明確指出,The setting innodb_autoinc_lock_mode=0 should not be used except for compatibility purposes.
除非出于兼容性目的,否則不應設置innodb_autoinc_lock_mode=0
。所以我們還是老老實實手動捕捉異常,慎用insert ignore
**innodb_autoinc_lock_mode: **在MySQL8中, 默認值為 2 (輕量級鎖) , 在MySQL8之前, 5.1之后, 默認值為 1(混合使用這2種鎖), 在更早的版本是 0(auto_inc鎖)
去重表
去重表,其實也是唯一索引方案的一個變種,原表不太適合再新建唯一索引了,且數(shù)據(jù)量不大的話。我們可以再新建一張去重表,把唯一標識作為唯一索引,然后把對原表的操作和同時新增去重表 ,放在一個事務中,如果重復創(chuàng)建,去重表會拋出唯一約束異常,事務里所有的操作就會回滾。
insert中加入exist條件判斷
有時候我們會遇到非常復雜的表,表結(jié)構(gòu)確定了,比如已經(jīng)有了許多索引字段,不太適合再新建索引的時候,呼嚕嚕 在這里再提供一個"騷操作":可以通過insert中加入exist來解決重復插入的問題。
比如:
insert into order(id,code,password)
select ${id},${code},${password}
from order
where not exists(select 1 from order where code = ${code}) limit 0,1;
上面的sql注意思路就是將查詢和插入寫在同一個sql中,需要注意的是limit 0,1
最后一定要加上,不然可能會出現(xiàn)重復插入的情況
悲觀鎖
悲觀鎖,顧名思義就是,對數(shù)據(jù)被外界或者內(nèi)部修改處理時,持"悲觀"態(tài)度,總認為會發(fā)生并發(fā)沖突,所以會在整個數(shù)據(jù)處理過程中,將數(shù)據(jù)鎖定。
悲觀鎖的實現(xiàn),通常依靠數(shù)據(jù)庫提供的鎖機制實現(xiàn),在這里以mysql為例,最典型的就是"for update"。
select * from order where id = "xxxx" for update;
需要注意的是:使用悲觀鎖,需要先關閉mysql的自動提交功能,將 set autocommit = 0;
for update
僅適用于Mysql中l(wèi)nnoDB引擎,默認是行級鎖,如果sql中有明確指定的主鍵時候,是行級鎖,如果沒有,會鎖表(非常危險的操作)。for update
一般和事務配合使用,一旦用戶對某個行施加了行級加鎖,則該用戶可以查詢也可以更新被加鎖的數(shù)據(jù)行,其它用戶只能查詢但不能更新被加鎖的數(shù)據(jù)行。直到顯示提交事務(由于關閉了mysql的自動提交)時,for update
獲取的鎖會自動釋放。
悲觀鎖雖然保證了數(shù)據(jù)處理的安全性,但會嚴重影響并發(fā)效率,降低系統(tǒng)吞吐量。適用于并發(fā)量不大、又對數(shù)據(jù)一致性比較高的場景。
樂觀鎖
樂觀鎖,和悲觀鎖相反,對數(shù)據(jù)被外界或者內(nèi)部修改處理時,持"樂觀"態(tài)度,總認為不會發(fā)生并發(fā)沖突,所以不會上鎖,只需在更新的時候會去判斷一下在此期間有沒有去更新這個數(shù)據(jù)。
一般是使用版本號或者時間戳,比如
- 我們在數(shù)據(jù)庫中,給訂單表增加一個version 字段
- select數(shù)據(jù)時,將version一起讀出,當提交數(shù)據(jù)更新時,判斷版本號是否和取出來的是否一致。如果不一致就代表,已更新,那就不更新。如果一致就繼續(xù)執(zhí)行更新操作。
- 每次更新時,除了更新指定的字段,也要將version進行+1操作
update order set name=#{xxx},version=#{version} where id=#{id} and version < ${version}
不加鎖就能保證冪等性,又增加了系統(tǒng)吞吐量,如果頻繁觸發(fā)版本號不一致的情況,反而降低了性能。
狀態(tài)機
狀態(tài)機也是樂觀鎖的一種,比如企業(yè)級貨品管理系統(tǒng)中,訂單的轉(zhuǎn)單流程,將訂單的狀態(tài),設置為有限的幾個(1-下單、2-已支付、3-完成、4-發(fā)貨、5-退貨),通過各個狀態(tài)依次執(zhí)行轉(zhuǎn)換,來控制訂單轉(zhuǎn)單的流程,是非常好的選擇。
分布式鎖
上面介紹了許多方案,在單體應用中是沒啥問題的,但是隨著時代的發(fā)展,現(xiàn)在微服務大行其道,以上方法就不太適應了。
在分布式系統(tǒng)中,上面唯一索引對于全局來說,是無法確定的,我們可以引入第三方分布式鎖來保證冪等性設計。分布式鎖,主要是用來 當多個進程不在同一個系統(tǒng)中,用分布式鎖控制多個進程對資源的訪問
實現(xiàn)分布式鎖常見的方法有:基于redis實現(xiàn)分布式鎖,基于 Consul 實現(xiàn)分布式鎖,基于 zookeeper實現(xiàn)分布式鎖等等,本文重點介紹最常見的基于redis實現(xiàn)分布式鎖,set NX PX + Lua
- 在分布式系統(tǒng)中,插入或者更新的請求,業(yè)務邏輯中先獲取唯一業(yè)務字段,比如訂單id之類的,接著需要獲取分布式鎖,對redis執(zhí)行下述命令
SET key value NX PX 30000
各參數(shù)的含義:
- SET: 在Redis 2.6.12之后,
set命令
整合了setex命令
的功能,支持了原子命令加鎖和設置過期時間的功能 - key:業(yè)務邏輯中先獲取唯一業(yè)務字段,比如訂單id,code之類,也可以在前面加一些系統(tǒng)參數(shù)當前綴,這個完全可以自定義
- value: 填入是一串隨機值,必須保證全局唯一性(在釋放鎖時,我們需要對value進行驗證,防止誤釋放),一般用uuid來實現(xiàn)
- NX: 表示key不存在時才設置,如果存在則返回 null。還有另一個參數(shù)XX,表示key存在時才設置,如果不存在則返回NULL
- PX 30000: 表示過期時間30000毫秒,指到30秒后,key將被自動刪除。這個非常的重要,如果設置過短,無法有效的防止重復請求,過長的話會浪費redis的空間
- 然后執(zhí)行插入或者更新,或者其他相關業(yè)務邏輯,在釋放鎖之前,如果有其他中心的服務來請求,由于key是一樣的,無法獲取鎖,就代表這些是重復請求,不操作,直接返回
- 執(zhí)行完插入或者更新后,需要釋放鎖,一定要判斷釋放的鎖的value和與Redis內(nèi)存儲的value是否一致,不然如果直接刪除的話,會把其他中心服務的鎖釋放調(diào)。
這種先查再刪的2步操作,我們可以使用lua腳本,把他們變成一個"原子操作"
Lua 是一種輕量小巧的腳本語言,Redis會將整個腳本作為一個整體執(zhí)行,中間不會被其他命令打斷插入(l類似與事務),可以減少網(wǎng)絡開銷,方便復用
以下是Lua腳本,通過 Redis 的 eval/evalsha 命令來運行:
if redis.call('get', KEYS[1]) == ARGV[1] //判斷value是否一致
then
return redis.call('del', KEYS[1])//刪除key
else
return 0
end
這樣依靠單體的redis實現(xiàn)的分布式鎖能夠很好的解決,微服務系統(tǒng)的冪等問題。但是有些公司的微服務更加龐大,redis也是集群的話,set NX PX + Lua
就不夠看了,這里介紹Redis作者推薦的方法-Redlock算法,這里就先不展開講了,不然文章篇幅過長。先挖個坑,后面有空填一下:)
token機制
最后再補充一個方案利用token機制,每次調(diào)用接口時,使用token來標識請求的唯一性。token也叫令牌,天然適合微服務?;趖oken+redis來設計冪等的思路還是比較簡單的,和分布式鎖類似:
- 客戶端發(fā)送請求,得去服務端獲取一個全局唯一的一串隨機字符串作為Token 令牌(每次請求獲取到的都是一個全新的令牌),把令牌保存到 redis 中,需要有過期時間,同時把這個 ID 返回給客戶端
- 客戶端第二次調(diào)用業(yè)務請求的時候必須攜帶這個 token,服務端會去校驗redis中是否有該token。如果存在,表示這是第一次請求,刪除緩存中的token(這邊還是建議用lua腳本,保證操作的原子性);如果緩存中不存在,表示重復請求,直接返回。
尾語
冪等性是系統(tǒng)服務對外一種承諾,特別業(yè)務中涉及的錢的部分,一定要慎重再慎重。雖然前端做限制會更容易點,但前后端都需要做努力,除了本文介紹的常見的方案,大家也可以集思廣益,畢竟技術在發(fā)展,單體到集群分布式,還會繼續(xù)發(fā)展,還有有新的問題產(chǎn)生。
本文雖然通篇在將冪等的重要性和如何實現(xiàn)冪等,但不可否認,冪等肯定導致系統(tǒng)吞吐量、并發(fā)能力的下降,企業(yè)級應用還是得根據(jù)業(yè)務,權(quán)衡利弊,感謝大家的閱讀。
參考資料:
https://www.cnblogs.com/linjiqin/p/9678022.html
全文完,感謝您的閱讀,如果我的文章對你有所幫助的話,還請點個免費的贊,你的支持會激勵我輸出更高質(zhì)量的文章,感謝!文章來源:http://www.zghlxwxcb.cn/news/detail-711606.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-711606.html
到了這里,關于如何保證接口的冪等性?的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!