十年網(wǎng)站開發(fā)經(jīng)驗(yàn) + 多家企業(yè)客戶 + 靠譜的建站團(tuán)隊(duì)
量身定制 + 運(yùn)營維護(hù)+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
10年積累的成都做網(wǎng)站、成都網(wǎng)站制作經(jīng)驗(yàn),可以快速應(yīng)對客戶對網(wǎng)站的新想法和需求。提供各種問題對應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認(rèn)識你,你也不認(rèn)識我。但先網(wǎng)站制作后付款的網(wǎng)站建設(shè)流程,更有臨汾免費(fèi)網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
前言:
隨著微處理機(jī)技術(shù)的發(fā)展,人們只需花幾百美元就能買到一個(gè)CPU芯片,這個(gè)芯片每秒鐘執(zhí)行的指令比80年代最大的大型機(jī)的處理機(jī)每秒鐘所執(zhí)行的指令還多。如果你愿意付出兩倍的價(jià)錢,將得到同樣的CPU,但它卻以更高的時(shí)鐘速率運(yùn)行。因此,最節(jié)約成本的辦法通常是在一個(gè)系統(tǒng)中使用集中在一起的大量的廉價(jià)CPU。所以,傾向于分布式系統(tǒng)的主要原因是它可以潛在地得到比單個(gè)的大型集中式系統(tǒng)好得多的性價(jià)比。實(shí)際上,分布式系統(tǒng)是通過較低廉的價(jià)格來實(shí)現(xiàn)相似的性能的。
隨著互聯(lián)網(wǎng)的興起,越來越多的人使用者互聯(lián)網(wǎng)產(chǎn)品。一般互聯(lián)網(wǎng)系統(tǒng)都是分布式部署的,分布式部署確實(shí)能帶來性能和效率上的提升,提升效率的同事,我們還需要注意,保證一個(gè)分布式環(huán)境下數(shù)據(jù)一致性的問題。
分布式鎖簡述
在單機(jī)時(shí)代,雖然不存在分布式鎖,但也會面臨資源互斥的情況,只不過在單機(jī)的情況下,如果有多個(gè)線程要同時(shí)訪問某個(gè)共享資源的時(shí)候,我們可以采用線程間加鎖的機(jī)制,即當(dāng)某個(gè)線程獲取到這個(gè)資源后,就需要對這個(gè)資源進(jìn)行加鎖,當(dāng)使用完資源之后,再解鎖,其它線程就可以接著使用了。例如,在JAVA中,甚至專門提供了一些處理鎖機(jī)制的一些API(synchronize/Lock等)。
但是到了分布式系統(tǒng)的時(shí)代,這種線程之間的鎖機(jī)制,就沒作用了,系統(tǒng)可能會有多份并且部署在不同的機(jī)器上,這些資源已經(jīng)不是在線程之間共享了,而是屬于進(jìn)程之間共享的資源。因此,為了解決這個(gè)問題,「分布式鎖」就強(qiáng)勢登場了。
分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。在分布式系統(tǒng)中,常常需要協(xié)調(diào)他們的動(dòng)作。如果不同的系統(tǒng)或是同一個(gè)系統(tǒng)的不同主機(jī)之間共享了一個(gè)或一組資源,那么訪問這些資源的時(shí)候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分布式鎖。
在分布式系統(tǒng)中,常常需要協(xié)調(diào)他們的動(dòng)作。如果不同的系統(tǒng)或是同一個(gè)系統(tǒng)的不同主機(jī)之間共享了一個(gè)或一組資源,那么訪問這些資源的時(shí)候,往往需要互斥來防止彼此干擾來保證一致性,這個(gè)時(shí)候,便需要使用到分布式鎖。
分布式鎖要滿足哪些要求呢?
排他性: 在同一時(shí)間只會有一個(gè)客戶端能獲取到鎖,其它客戶端無法同時(shí)獲取
避免死鎖: 這把鎖在一段有限的時(shí)間之后,一定會被釋放(正常釋放或異常釋放)
高可用: 獲取或釋放鎖的機(jī)制必須高可用且性能佳
...
目前相對主流的有三種,從實(shí)現(xiàn)的復(fù)雜度上來看,從上往下難度依次增加:
數(shù)據(jù)庫(MySQL)
redis
ZooKeeper
v 基于數(shù)據(jù)庫實(shí)現(xiàn)
基于數(shù)據(jù)庫來做分布式鎖的話,通常有兩種做法:
基于數(shù)據(jù)庫的樂觀鎖
基于數(shù)據(jù)庫的悲觀鎖
樂觀鎖
樂觀鎖的特點(diǎn)先進(jìn)行業(yè)務(wù)操作,不到萬不得已不去拿鎖。即“樂觀”的認(rèn)為拿鎖多半是會成功的,因此在進(jìn)行完業(yè)務(wù)操作需要實(shí)際更新數(shù)據(jù)的最后一步再去拿一下鎖就好。
樂觀鎖機(jī)制其實(shí)就是在數(shù)據(jù)庫表中引入一個(gè)版本號(version)字段來實(shí)現(xiàn)的。當(dāng)我們要從數(shù)據(jù)庫中讀取數(shù)據(jù)的時(shí)候,同時(shí)把這個(gè)version字段也讀出來,如果要對讀出來的數(shù)據(jù)進(jìn)行更新后寫回?cái)?shù)據(jù)庫,則需要將version加1,同時(shí)將新的數(shù)據(jù)與新的version更新到數(shù)據(jù)表中,且必須在更新的時(shí)候同時(shí)檢查目前數(shù)據(jù)庫里version值是不是之前的那個(gè)version,如果是,則正常更新。如果不是,則更新失敗,說明在這個(gè)過程中有其它的進(jìn)程去更新過數(shù)據(jù)了。
看圖敘事。模擬實(shí)戰(zhàn)場景。
如上圖,故事男主人公(以下簡稱男主)打算去ATM機(jī)取3000元,故事女主人公(以下簡稱女主)則要在某寶買買買,買個(gè)包需要3000元,賬戶的余額是5000元。如果沒有采用鎖的話,在兩人同時(shí)取款和買買買,可能會出現(xiàn)合計(jì)消費(fèi)了6000,導(dǎo)致賬戶余額異常。所以需要用到鎖的機(jī)制,當(dāng)男主女主甚至更多小主同時(shí)消費(fèi)時(shí),除了讀取到6000的賬戶余額外,還需要讀取到當(dāng)前的版本號version=1,等先行消費(fèi)成功的主人公(無論誰先消費(fèi))去出發(fā)修改賬戶余額的同時(shí),會觸發(fā)version=version+1,即version=2。那么其他人使用未更新的version(1)去更新賬戶余額時(shí)就會發(fā)現(xiàn)版本號不對,就會導(dǎo)致本次更新失敗,就得重新去讀取最新賬戶余額以及版本號。
樂觀鎖遵循的兩點(diǎn)法則:
鎖服務(wù)要有遞增的版本號version
每次更新數(shù)據(jù)的時(shí)候都必須先判斷版本號對不對,然后再寫入新的版本號
悲觀鎖
悲觀鎖的特點(diǎn)是先獲取鎖,再進(jìn)行業(yè)務(wù)操作,即“悲觀”的認(rèn)為獲取鎖是非常有可能失敗的,因此要先確保獲取鎖成功再進(jìn)行業(yè)務(wù)操作。
通常所說的“一鎖二查三更新”即指的是使用悲觀鎖。通常來講在數(shù)據(jù)庫上的悲觀鎖需要數(shù)據(jù)庫本身提供支持,即通過常用的 select ... for update 操作來實(shí)現(xiàn)悲觀鎖。當(dāng)數(shù)據(jù)庫執(zhí)行 select for update 時(shí)會獲取被 select 中的數(shù)據(jù)行的行鎖,因此其他并發(fā)執(zhí)行的 select for update 如果試圖選中同一行則會發(fā)生排斥(需要等待行鎖被釋放),因此達(dá)到鎖的效果。 select for update 獲取的行鎖會在當(dāng)前事務(wù)結(jié)束時(shí)自動(dòng)釋放,因此必須在事務(wù)中使用。
示例:
/** * 消費(fèi)以后更新銀行余額 * @param bankId 銀行卡號 * @param cost 消費(fèi)金額 * @return */ public boolean consume(Long bankId, Integer cost){ //先鎖定銀行賬戶 BankAccount product = query("SELECT * FROM bank_account WHERE bank_id=#{bankId} FOR UPDATE", bankId); if (product.getNumber() > 0) { int updateCnt = update("UPDATE tb_product_stock SET number=#{cost} WHERE product_id=#{productId}", cost, bankId); if(updateCnt > 0){ //更新庫存成功 return true; } } return false; }
樂觀鎖與悲觀鎖的區(qū)別
樂觀鎖的思路一般是表中增加版本字段,更新時(shí)where語句中增加版本的判斷,算是一種CAS(Compare And Swep)操作,銀行消費(fèi)場景中version起到了版本控制的作用( AND version=#{version} )。
悲觀鎖之所以是悲觀,在于他認(rèn)為本次操作會發(fā)生并發(fā)沖突,所以一開始就對銀行賬戶加上鎖( SELECT ... FOR UPDATE ),然后就可以安心的做判斷和更新,因?yàn)檫@時(shí)候不會有別人更新賬戶余額。
v 基于Redis實(shí)現(xiàn)
基于Redis實(shí)現(xiàn)的鎖機(jī)制,主要是依賴redis自身的原子操作,例如:
SET user_key user_value NX PX 100
redis從2.6.12版本開始,SET命令才支持這些參數(shù):
NX:只在在鍵不存在時(shí),才對鍵進(jìn)行設(shè)置操作, SET key value NX 效果等同于 SETNX key value
PX millisecond:設(shè)置鍵的過期時(shí)間為millisecond毫秒,當(dāng)超過這個(gè)時(shí)間后,設(shè)置的鍵會自動(dòng)失效
上述代碼示例是指,當(dāng)redis中不存在user_key這個(gè)鍵的時(shí)候,才會去設(shè)置一個(gè)user_key鍵,并且給這個(gè)鍵的值設(shè)置為 user_value,且這個(gè)鍵的存活時(shí)間為100ms
為什么這個(gè)命令可以幫我們實(shí)現(xiàn)鎖機(jī)制呢?
因?yàn)檫@個(gè)命令是只有在某個(gè)key不存在的時(shí)候,才會執(zhí)行成功。那么當(dāng)多個(gè)進(jìn)程同時(shí)并發(fā)的去設(shè)置同一個(gè)key的時(shí)候,就永遠(yuǎn)只會有一個(gè)進(jìn)程成功。當(dāng)某個(gè)進(jìn)程設(shè)置成功之后,就可以去執(zhí)行業(yè)務(wù)邏輯了,等業(yè)務(wù)邏輯執(zhí)行完畢之后,再去進(jìn)行解鎖。
解鎖很簡單,只需要?jiǎng)h除這個(gè)key就可以了,不過刪除之前需要判斷,這個(gè)key對應(yīng)的value是當(dāng)初自己設(shè)置的那個(gè)。
另外,針對redis集群模式的分布式鎖,可以采用redis的 Redlock (可能會被墻)機(jī)制。
v 基于ZooKeeper實(shí)現(xiàn)
其實(shí)基于ZooKeeper,就是使用它的臨時(shí)有序節(jié)點(diǎn)來實(shí)現(xiàn)的分布式鎖。
原理
當(dāng)某客戶端要進(jìn)行邏輯的加鎖時(shí),就在zookeeper上的某個(gè)指定節(jié)點(diǎn)的目錄下,去生成一個(gè)唯一的臨時(shí)有序節(jié)點(diǎn), 然后判斷自己是否是這些有序節(jié)點(diǎn)中序號最小的一個(gè),如果是,則算是獲取了鎖。如果不是,則說明沒有獲取到鎖,那么就需要在序列中找到比自己小的那個(gè)節(jié)點(diǎn),并對其調(diào)用 exist() 方法,對其注冊事件監(jiān)聽,當(dāng)監(jiān)聽到這個(gè)節(jié)點(diǎn)被刪除了,那就再去判斷一次自己當(dāng)初創(chuàng)建的節(jié)點(diǎn)是否變成了序列中最小的。如果是,則獲取鎖,如果不是,則重復(fù)上述步驟。
當(dāng)釋放鎖的時(shí)候,只需將這個(gè)臨時(shí)節(jié)點(diǎn)刪除即可。
如上圖,locker是一個(gè)持久節(jié)點(diǎn), node_1/node_2/.../node_n 就是上面說的臨時(shí)節(jié)點(diǎn),由客戶端client去創(chuàng)建的。
client_1/client_2/.../clien_n 都是想去獲取鎖的客戶端。以client_1為例,它想去獲取分布式鎖,則需要跑到locker下面去創(chuàng)建臨時(shí)節(jié)點(diǎn)(假如是node_1)創(chuàng)建完畢后,看一下自己的節(jié)點(diǎn)序號是否是locker下面最小的,如果是,則獲取了鎖。如果不是,則去找到比自己小的那個(gè)節(jié)點(diǎn)(假如是node_2),找到后,就監(jiān)聽node_2,直到node_2被刪除,那么就開始再次判斷自己的node_1是不是序列中最小的,如果是,則獲取鎖,如果還不是,則繼續(xù)找一下一個(gè)節(jié)點(diǎn)。
總結(jié):
分布式鎖有很多種,開篇說的"相對主流的有三種"只是針對我所遇到的。分布式鎖未來肯定是千變?nèi)f化的,無論你身處一個(gè)什么樣的公司,最開始的工作可能都得盡可能的從簡單的做起。希望大家能根據(jù)所在公司業(yè)務(wù)場景,選擇適合所在項(xiàng)目的方案。
順便在此給大家推薦一個(gè)Java架構(gòu)方面的交流學(xué)習(xí)群: 698581634 ,里面會分享一些資深架構(gòu)師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發(fā)、高性能、分布式、微服務(wù)架構(gòu)的原理,JVM性能優(yōu)化這些成為架構(gòu)師必備的知識體系,主要針對Java開發(fā)人員提升自己,突破瓶頸,相信你來學(xué)習(xí),會有提升和收獲。在這個(gè)群里會有你需要的內(nèi)容 朋友們請抓緊時(shí)間加入進(jìn)來吧。