目前幾乎很多大型網(wǎng)站及應用都是分布式部署的,分布式場景中的數(shù)據(jù)一致性問題一直是一個比較重要的話題。分布式的CAP理論告訴我們“任何一個分布式系統(tǒng)都無法同時滿足一致性(Consistency)、可用性(Availability)和分區(qū)容錯性(Partition tolerance),最多只能同時滿足兩項。”所以,很多系統(tǒng)在設計之初就要對這三者做出取舍态坦。在互聯(lián)網(wǎng)領域的絕大多數(shù)的場景中谜诫,都需要犧牲強一致性來換取系統(tǒng)的高可用性生逸,系統(tǒng)往往只需要保證“最終一致性”,只要這個最終時間是在用戶可以接受的范圍內即可且预。
在很多場景中槽袄,我們?yōu)榱吮WC數(shù)據(jù)的最終一致性,需要很多的技術方案來支持锋谐,比如分布式事務遍尺、分布式鎖等。有的時候涮拗,我們需要保證一個方法在同一時間內只能被同一個線程執(zhí)行乾戏。在單機環(huán)境中,Java中其實提供了很多并發(fā)處理相關的API多搀,但是這些API在分布式場景中就無能為力了歧蕉。也就是說單純的Java Api并不能提供分布式鎖的能力。所以針對分布式鎖的實現(xiàn)目前有多種方案康铭。
針對分布式鎖的實現(xiàn)惯退,目前比較常用的有以下幾種方案:
- 基于數(shù)據(jù)庫實現(xiàn)分布式鎖
- 基于緩存實現(xiàn)分布式鎖
- 基于Zookeeper實現(xiàn)分布式鎖
在分析這幾種實現(xiàn)方案之前我們先來想一下,我們需要的分布式鎖應該是怎么樣的从藤?(這里以方法鎖為例催跪,資源鎖同理)
- 可以保證在分布式部署的應用集群中,同一個方法在同一時間只能被一臺機器上的一個線程執(zhí)行夷野。
- 這把鎖要是一把可重入鎖(避免死鎖)
- 這把鎖最好是一把阻塞鎖(根據(jù)業(yè)務需求考慮要不要這條)
- 有高可用的獲取鎖和釋放鎖功能
- 獲取鎖和釋放鎖的性能要好
- 基于數(shù)據(jù)庫實現(xiàn)分布式鎖
一懊蒸、基于數(shù)據(jù)庫表
要實現(xiàn)分布式鎖,最簡單的方式可能就是直接創(chuàng)建一張鎖表悯搔,然后通過操作該表中的數(shù)據(jù)來實現(xiàn)了骑丸。
當我們要鎖住某個方法或資源時,我們就在該表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄通危。
創(chuàng)建這樣一張數(shù)據(jù)庫表:
CREATE TABLE `methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的方法名',
`desc` varchar(1024) NOT NULL DEFAULT '備注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存數(shù)據(jù)時間铸豁,自動生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
當我們想要鎖住某個方法時,執(zhí)行以下SQL:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
因為我們對method_name做了唯一性約束菊碟,這里如果有多個請求同時提交到數(shù)據(jù)庫的話节芥,數(shù)據(jù)庫會保證只有一個操作可以成功,那么我們就可以認為操作成功的那個線程獲得了該方法的鎖逆害,可以執(zhí)行方法體內容头镊。
當方法執(zhí)行完畢之后,想要釋放鎖的話魄幕,需要執(zhí)行以下Sql:
delete from methodLock where method_name ='method_name'
上面這種簡單的實現(xiàn)有以下幾個問題:
- 這把鎖強依賴數(shù)據(jù)庫的可用性相艇,數(shù)據(jù)庫是一個單點,一旦數(shù)據(jù)庫掛掉梅垄,會導致業(yè)務系統(tǒng)不可用厂捞。
- 這把鎖沒有失效時間,一旦解鎖操作失敗队丝,就會導致鎖記錄一直在數(shù)據(jù)庫中,其他線程無法再獲得到鎖欲鹏。
- 這把鎖只能是非阻塞的机久,因為數(shù)據(jù)的insert操作,一旦插入失敗就會直接報錯赔嚎。沒有獲得鎖的線程并不會進入排隊隊列膘盖,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作。
- 這把鎖是非重入的尤误,同一個線程在沒有釋放鎖之前無法再次獲得該鎖侠畔。因為數(shù)據(jù)中數(shù)據(jù)已經(jīng)存在了。當然损晤,我們也可以有其他方式解決上面的問題软棺。
- 數(shù)據(jù)庫是單點?搞兩個數(shù)據(jù)庫尤勋,數(shù)據(jù)之前雙向同步喘落。一旦掛掉快速切換到備庫上。
- 沒有失效時間最冰?只要做一個定時任務瘦棋,每隔一定時間把數(shù)據(jù)庫中的超時數(shù)據(jù)清理一遍。
- 非阻塞的暖哨?搞一個while循環(huán)赌朋,直到insert成功再返回成功。
- 非重入的?在數(shù)據(jù)庫表中加個字段沛慢,記錄當前獲得鎖的機器的主機信息和線程信息服球,那么下次再獲取鎖的時候先查詢數(shù)據(jù)庫,如果當前機器的主機信息和線程信息在數(shù)據(jù)庫可以查到的話颠焦,直接把鎖分配給他就可以了斩熊。
Ⅰ. 基于數(shù)據(jù)庫排他鎖
除了可以通過增刪操作數(shù)據(jù)表中的記錄以外,其實還可以借助數(shù)據(jù)中自帶的鎖來實現(xiàn)分布式的鎖伐庭。
我們還用剛剛創(chuàng)建的那張數(shù)據(jù)庫表粉渠。可以通過數(shù)據(jù)庫的排他鎖來實現(xiàn)分布式鎖圾另。 基于MySql的InnoDB引擎霸株,可以使用以下方法來實現(xiàn)加鎖操作:
public boolean lock(){
connection.setAutoCommit(false)
while(true){
try{
result = select * from methodLock where method_name=xxx for update;
if(result==null){
return true;
}
}catch(Exception e){
}
sleep(1000);
}
return false;
}
在查詢語句后面增加for update,數(shù)據(jù)庫會在查詢過程中給數(shù)據(jù)庫表增加排他鎖(這里再多提一句集乔,InnoDB引擎在加鎖的時候去件,只有通過索引進行檢索的時候才會使用行級鎖,否則會使用表級鎖扰路。這里我們希望使用行級鎖尤溜,就要給method_name添加索引,值得注意的是汗唱,這個索引一定要創(chuàng)建成唯一索引宫莱,否則會出現(xiàn)多個重載方法之間無法同時被訪問的問題。重載方法的話建議把參數(shù)類型也加上哩罪。)授霸。當某條記錄被加上排他鎖之后,其他線程無法再在該行記錄上增加排他鎖际插。
我們可以認為獲得排它鎖的線程即可獲得分布式鎖碘耳,當獲取到鎖之后,可以執(zhí)行方法的業(yè)務邏輯框弛,執(zhí)行完方法之后辛辨,再通過以下方法解鎖:
public void unlock(){
connection.commit();
}
通過connection.commit()操作來釋放鎖。
這種方法可以有效的解決上面提到的無法釋放鎖和阻塞鎖的問題功咒。
- 阻塞鎖愉阎? for update語句會在執(zhí)行成功后立即返回,在執(zhí)行失敗時一直處于阻塞狀態(tài)力奋,直到成功榜旦。
- 鎖定之后服務宕機,無法釋放景殷?使用這種方式溅呢,服務宕機之后數(shù)據(jù)庫會自己把鎖釋放掉澡屡。
但是還是無法直接解決數(shù)據(jù)庫單點和可重入問題。
這里還可能存在另外一個問題咐旧,雖然我們對method_name 使用了唯一索引驶鹉,并且顯示使用for update來使用行級鎖。但是铣墨,MySql會對查詢進行優(yōu)化室埋,即便在條件中使用了索引字段,但是否使用索引來檢索數(shù)據(jù)是由 MySQL 通過判斷不同執(zhí)行計劃的代價來決定的伊约,如果 MySQL 認為全表掃效率更高姚淆,比如對一些很小的表,它就不會使用索引屡律,這種情況下 InnoDB 將使用表鎖腌逢,而不是行鎖。如果發(fā)生這種情況就悲劇了超埋。搏讶。。
還有一個問題霍殴,就是我們要使用排他鎖來進行分布式鎖的lock媒惕,那么一個排他鎖長時間不提交,就會占用數(shù)據(jù)庫連接繁成。一旦類似的連接變得多了吓笙,就可能把數(shù)據(jù)庫連接池撐爆
Ⅱ. 小結
總結一下使用數(shù)據(jù)庫來實現(xiàn)分布式鎖的方式,這兩種方式都是依賴數(shù)據(jù)庫的一張表巾腕,一種是通過表中的記錄的存在情況確定當前是否有鎖存在,另外一種是通過數(shù)據(jù)庫的排他鎖來實現(xiàn)分布式鎖絮蒿。
數(shù)據(jù)庫實現(xiàn)分布式鎖的優(yōu)點
直接借助數(shù)據(jù)庫尊搬,容易理解。
數(shù)據(jù)庫實現(xiàn)分布式鎖的缺點
會有各種各樣的問題土涝,在解決問題的過程中會使整個方案變得越來越復雜佛寿。
操作數(shù)據(jù)庫需要一定的開銷,性能問題需要考慮但壮。
使用數(shù)據(jù)庫的行級鎖并不一定靠譜冀泻,尤其是當我們的鎖表并不大的時候。
二蜡饵、基于緩存實現(xiàn)分布式鎖
相比較于基于數(shù)據(jù)庫實現(xiàn)分布式鎖的方案來說弹渔,基于緩存來實現(xiàn)在性能方面會表現(xiàn)的更好一點。而且很多緩存是可以集群部署的溯祸,可以解決單點問題肢专。
目前有很多成熟的緩存產(chǎn)品舞肆,包括Redis,memcached以及我們公司內部的Tair博杖。
這里以Tair為例來分析下使用緩存實現(xiàn)分布式鎖的方案椿胯。關于Redis和memcached在網(wǎng)絡上有很多相關的文章,并且也有一些成熟的框架及算法可以直接使用剃根。
基于Tair的實現(xiàn)分布式鎖其實和Redis類似哩盲,其中主要的實現(xiàn)方式是使用TairManager.put方法來實現(xiàn)。
public boolean trylock(String key) {
ResultCode code = ldbTairManager.put(NAMESPACE, key, "This is a Lock.", 2, 0);
if (ResultCode.SUCCESS.equals(code))
return true;
else
return false;
}
public boolean unlock(String key) {
ldbTairManager.invalid(NAMESPACE, key);
}
以上實現(xiàn)方式同樣存在幾個問題:
- 這把鎖沒有失效時間狈醉,一旦解鎖操作失敗廉油,就會導致鎖記錄一直在tair中,其他線程無法再獲得到鎖舔糖。
- 這把鎖只能是非阻塞的娱两,無論成功還是失敗都直接返回。
- 這把鎖是非重入的金吗,一個線程獲得鎖之后十兢,在釋放鎖之前,無法再次獲得該鎖摇庙,因為使用到的key在tair中已經(jīng)存在旱物。無法再執(zhí)行put操作。
當然卫袒,同樣有方式可以解決宵呛。
- 沒有失效時間?tair的put方法支持傳入失效時間夕凝,到達時間之后數(shù)據(jù)會自動刪除宝穗。
- 非阻塞?while重復執(zhí)行码秉。
- 非可重入逮矛?在一個線程獲取到鎖之后,把當前主機信息和線程信息保存起來转砖,下次再獲取之前先檢查自己是不是當前鎖的擁有者须鼎。
但是,失效時間我設置多長時間為好府蔗?如何設置的失效時間太短晋控,方法沒等執(zhí)行完,鎖就自動釋放了姓赤,那么就會產(chǎn)生并發(fā)問題赡译。如果設置的時間太長,其他獲取鎖的線程就可能要平白的多等一段時間模捂。這個問題使用數(shù)據(jù)庫實現(xiàn)分布式鎖同樣存在
小結
可以使用緩存來代替數(shù)據(jù)庫來實現(xiàn)分布式鎖捶朵,這個可以提供更好的性能蜘矢,同時,很多緩存服務都是集群部署的综看,可以避免單點問題品腹。并且很多緩存服務都提供了可以用來實現(xiàn)分布式鎖的方法,比如Tair的put方法红碑,redis的setnx方法等舞吭。并且,這些緩存服務也都提供了對數(shù)據(jù)的過期自動刪除的支持析珊,可以直接設置超時時間來控制鎖的釋放羡鸥。
使用緩存實現(xiàn)分布式鎖的優(yōu)點
性能好,實現(xiàn)起來較為方便忠寻。
使用緩存實現(xiàn)分布式鎖的缺點
通過超時時間來控制鎖的失效時間并不是十分的靠譜惧浴。
三、基于Zookeeper實現(xiàn)分布式鎖
基于zookeeper臨時有序節(jié)點可以實現(xiàn)的分布式鎖奕剃。
大致思想即為:每個客戶端對某個方法加鎖時衷旅,在zookeeper上的與該方法對應的指定節(jié)點的目錄下,生成一個唯一的瞬時有序節(jié)點纵朋。 判斷是否獲取鎖的方式很簡單柿顶,只需要判斷有序節(jié)點中序號最小的一個。 當釋放鎖的時候操软,只需將這個瞬時節(jié)點刪除即可嘁锯。同時,其可以避免服務宕機導致的鎖無法釋放聂薪,而產(chǎn)生的死鎖問題家乘。
來看下Zookeeper能不能解決前面提到的問題。
- 鎖無法釋放藏澳?使用Zookeeper可以有效的解決鎖無法釋放的問題烤低,因為在創(chuàng)建鎖的時候,客戶端會在ZK中創(chuàng)建一個臨時節(jié)點笆载,一旦客戶端獲取到鎖之后突然掛掉(Session連接斷開),那么這個臨時節(jié)點就會自動刪除掉涯呻。其他客戶端就可以再次獲得鎖凉驻。
- 非阻塞鎖?使用Zookeeper可以實現(xiàn)阻塞的鎖复罐,客戶端可以通過在ZK中創(chuàng)建順序節(jié)點涝登,并且在節(jié)點上綁定監(jiān)聽器,一旦節(jié)點有變化效诅,Zookeeper會通知客戶端胀滚,客戶端可以檢查自己創(chuàng)建的節(jié)點是不是當前所有節(jié)點中序號最小的趟济,如果是,那么自己就獲取到鎖咽笼,便可以執(zhí)行業(yè)務邏輯了顷编。
- 不可重入?使用Zookeeper也可以有效的解決不可重入的問題剑刑,客戶端在創(chuàng)建節(jié)點的時候媳纬,把當前客戶端的主機信息和線程信息直接寫入到節(jié)點中,下次想要獲取鎖的時候和當前最小的節(jié)點中的數(shù)據(jù)比對一下就可以了施掏。如果和自己的信息一樣钮惠,那么自己直接獲取到鎖,如果不一樣就再創(chuàng)建一個臨時的順序節(jié)點七芭,參與排隊素挽。
- 單點問題?使用Zookeeper可以有效的解決單點問題狸驳,ZK是集群部署的预明,只要集群中有半數(shù)以上的機器存活,就可以對外提供服務锌历。
可以直接使用zookeeper第三方庫Curator客戶端贮庞,這個客戶端中封裝了一個可重入的鎖服務。
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
try {
return interProcessMutex.acquire(timeout, unit);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
public boolean unlock() {
try {
interProcessMutex.release();
} catch (Throwable e) {
log.error(e.getMessage(), e);
} finally {
executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
}
return true;
}
Curator提供的InterProcessMutex是分布式鎖的實現(xiàn)究西。acquire方法用戶獲取鎖窗慎,release方法用于釋放鎖。
使用ZK實現(xiàn)的分布式鎖好像完全符合了本文開頭我們對一個分布式鎖的所有期望卤材。但是遮斥,其實并不是,Zookeeper實現(xiàn)的分布式鎖其實存在一個缺點扇丛,那就是性能上可能并沒有緩存服務那么高术吗。因為每次在創(chuàng)建鎖和釋放鎖的過程中,都要動態(tài)創(chuàng)建帆精、銷毀瞬時節(jié)點來實現(xiàn)鎖功能较屿。ZK中創(chuàng)建和刪除節(jié)點只能通過Leader服務器來執(zhí)行,然后將數(shù)據(jù)同不到所有的Follower機器上卓练。
其實隘蝎,使用Zookeeper也有可能帶來并發(fā)問題,只是并不常見而已襟企≈雒矗考慮這樣的情況,由于網(wǎng)絡抖動顽悼,客戶端可ZK集群的session連接斷了曼振,那么zk以為客戶端掛了几迄,就會刪除臨時節(jié)點,這時候其他客戶端就可以獲取到分布式鎖了冰评。就可能產(chǎn)生并發(fā)問題映胁。這個問題不常見是因為zk有重試機制,一旦zk集群檢測不到客戶端的心跳集索,就會重試屿愚,Curator客戶端支持多種重試策略。多次重試之后還不行的話才會刪除臨時節(jié)點务荆。(所以妆距,選擇一個合適的重試策略也比較重要,要在鎖的粒度和并發(fā)之間找一個平衡函匕。)
小結
使用Zookeeper實現(xiàn)分布式鎖的優(yōu)點
有效的解決單點問題娱据,不可重入問題,非阻塞問題以及鎖無法釋放的問題盅惜。實現(xiàn)起來較為簡單中剩。
使用Zookeeper實現(xiàn)分布式鎖的缺點
性能上不如使用緩存實現(xiàn)分布式鎖。 需要對ZK的原理有所了解抒寂。
四结啼、三種方案的比較
上面幾種方式,哪種方式都無法做到完美屈芜。就像CAP一樣郊愧,在復雜性、可靠性井佑、性能等方面無法同時滿足属铁,所以,根據(jù)不同的應用場景選擇最適合自己的才是王道躬翁。
從理解的難易程度角度(從低到高)
數(shù)據(jù)庫 > 緩存 > Zookeeper
從實現(xiàn)的復雜性角度(從低到高)
Zookeeper >= 緩存 > 數(shù)據(jù)庫
從性能角度(從高到低)
緩存 > Zookeeper >= 數(shù)據(jù)庫
從可靠性角度(從高到低)
Zookeeper > 緩存 > 數(shù)據(jù)庫
寫在最后
- 第一:隨手點贊焦蘑,手留余香!
- ...
- 第二:分享知識盒发,傳播快樂例嘱!
- ...
- 第三:看完關注,每天更新宁舰!