溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

淺談分布式鎖的幾種使用方式(redis、zookeeper、數(shù)據(jù)庫(kù))

發(fā)布時(shí)間:2020-09-06 01:44:45 來(lái)源:腳本之家 閱讀:138 作者:南北雪樹(shù) 欄目:數(shù)據(jù)庫(kù)

Q:一個(gè)業(yè)務(wù)服務(wù)器,一個(gè)數(shù)據(jù)庫(kù),操作:查詢用戶當(dāng)前余額,扣除當(dāng)前余額的3%作為手續(xù)費(fèi)

  • synchronized
  • lock
  • dblock

Q:兩個(gè)業(yè)務(wù)服務(wù)器,一個(gè)數(shù)據(jù)庫(kù),操作:查詢用戶當(dāng)前余額,扣除當(dāng)前余額的3%作為手續(xù)費(fèi)

  • 分布式鎖

我們需要怎么樣的分布式鎖?

  • 可以保證在分布式部署的應(yīng)用集群中,同一個(gè)方法在同一時(shí)間只能被一臺(tái)機(jī)器上的一個(gè)線程執(zhí)行。
  • 這把鎖要是一把可重入鎖(避免死鎖)
  • 這把鎖最好是一把阻塞鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
  • 這把鎖最好是一把公平鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
  • 有高可用的獲取鎖和釋放鎖功能
  • 獲取鎖和釋放鎖的性能要好

一、基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)的分布式鎖

基于表實(shí)現(xiàn)的分布式鎖

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ù)時(shí)間,自動(dòng)生成', 
PRIMARY KEY (`id`), 
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';

當(dāng)我們想要鎖住某個(gè)方法時(shí),執(zhí)行以下SQL:
insert into methodLock(method_name,desc) values (‘method_name',‘desc')
因?yàn)槲覀儗?duì)method_name做了唯一性約束,這里如果有多個(gè)請(qǐng)求同時(shí)提交到數(shù)據(jù)庫(kù)的話,數(shù)據(jù)庫(kù)會(huì)保證只有一個(gè)操作可以成功,那么我們就可以認(rèn)為操作成功的那個(gè)線程獲得了該方法的鎖,可以執(zhí)行方法體內(nèi)容。

當(dāng)方法執(zhí)行完畢之后,想要釋放鎖的話,需要執(zhí)行以下Sql:
delete from methodLock where method_name ='method_name'

上面這種簡(jiǎn)單的實(shí)現(xiàn)有以下幾個(gè)問(wèn)題:

  • 這把鎖強(qiáng)依賴(lài)數(shù)據(jù)庫(kù)的可用性,數(shù)據(jù)庫(kù)是一個(gè)單點(diǎn),一旦數(shù)據(jù)庫(kù)掛掉,會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)不可用。
  • 這把鎖沒(méi)有失效時(shí)間,一旦解鎖操作失敗,就會(huì)導(dǎo)致鎖記錄一直在數(shù)據(jù)庫(kù)中,其他線程無(wú)法再獲得到鎖。
  • 這把鎖只能是非阻塞的,因?yàn)閿?shù)據(jù)的insert操作,一旦插入失敗就會(huì)直接報(bào)錯(cuò)。沒(méi)有獲得鎖的線程并不會(huì)進(jìn)入排隊(duì)隊(duì)列,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作。
  • 這把鎖是非重入的,同一個(gè)線程在沒(méi)有釋放鎖之前無(wú)法再次獲得該鎖。因?yàn)閿?shù)據(jù)中數(shù)據(jù)已經(jīng)存在了。
  • 這把鎖是非公平鎖,所有等待鎖的線程憑運(yùn)氣去爭(zhēng)奪鎖。

當(dāng)然,我們也可以有其他方式解決上面的問(wèn)題。

  • 數(shù)據(jù)庫(kù)是單點(diǎn)?搞兩個(gè)數(shù)據(jù)庫(kù),數(shù)據(jù)之前雙向同步。一旦掛掉快速切換到備庫(kù)上。
  • 沒(méi)有失效時(shí)間?只要做一個(gè)定時(shí)任務(wù),每隔一定時(shí)間把數(shù)據(jù)庫(kù)中的超時(shí)數(shù)據(jù)清理一遍。
  • 非阻塞的?搞一個(gè)while循環(huán),直到insert成功再返回成功。
  • 非重入的?在數(shù)據(jù)庫(kù)表中加個(gè)字段,記錄當(dāng)前獲得鎖的機(jī)器的主機(jī)信息和線程信息,那么下次再獲取鎖的時(shí)候先查詢數(shù)據(jù)庫(kù),如果當(dāng)前機(jī)器的主機(jī)信息和線程信息在數(shù)據(jù)庫(kù)可以查到的話,直接把鎖分配給他就可以了。
  • 非公平的?再建一張中間表,將等待鎖的線程全記錄下來(lái),并根據(jù)創(chuàng)建時(shí)間排序,只有最先創(chuàng)建的允許獲取鎖

基于排他鎖實(shí)現(xiàn)的分布式鎖

除了可以通過(guò)增刪操作數(shù)據(jù)表中的記錄以外,其實(shí)還可以借助數(shù)據(jù)中自帶的鎖來(lái)實(shí)現(xiàn)分布式的鎖。

我們還用剛剛創(chuàng)建的那張數(shù)據(jù)庫(kù)表。可以通過(guò)數(shù)據(jù)庫(kù)的排他鎖來(lái)實(shí)現(xiàn)分布式鎖。 基于MySql的InnoDB引擎,可以使用以下方法來(lái)實(shí)現(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;
}

在查詢語(yǔ)句后面增加for update,數(shù)據(jù)庫(kù)會(huì)在查詢過(guò)程中給數(shù)據(jù)庫(kù)表增加排他鎖。當(dāng)某條記錄被加上排他鎖之后,其他線程無(wú)法再在該行記錄上增加排他鎖。

我們可以認(rèn)為獲得排它鎖的線程即可獲得分布式鎖,當(dāng)獲取到鎖之后,可以執(zhí)行方法的業(yè)務(wù)邏輯,執(zhí)行完方法之后,再通過(guò)以下方法解鎖:

public void unlock(){ connection.commit(); }

通過(guò)connection.commit();操作來(lái)釋放鎖。

這種方法可以有效的解決上面提到的無(wú)法釋放鎖和阻塞鎖的問(wèn)題。

阻塞鎖? for update語(yǔ)句會(huì)在執(zhí)行成功后立即返回,在執(zhí)行失敗時(shí)一直處于阻塞狀態(tài),直到成功。

鎖定之后服務(wù)宕機(jī),無(wú)法釋放?使用這種方式,服務(wù)宕機(jī)之后數(shù)據(jù)庫(kù)會(huì)自己把鎖釋放掉。

但是還是無(wú)法直接解決數(shù)據(jù)庫(kù)單點(diǎn)、可重入和公平鎖的問(wèn)題。

總結(jié)一下使用數(shù)據(jù)庫(kù)來(lái)實(shí)現(xiàn)分布式鎖的方式,這兩種方式都是依賴(lài)數(shù)據(jù)庫(kù)的一張表,一種是通過(guò)表中的記錄的存在情況確定當(dāng)前是否有鎖存在,另外一種是通過(guò)數(shù)據(jù)庫(kù)的排他鎖來(lái)實(shí)現(xiàn)分布式鎖。

數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖的優(yōu)點(diǎn)

直接借助數(shù)據(jù)庫(kù),容易理解。

數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖的缺點(diǎn)

會(huì)有各種各樣的問(wèn)題,在解決問(wèn)題的過(guò)程中會(huì)使整個(gè)方案變得越來(lái)越復(fù)雜。

操作數(shù)據(jù)庫(kù)需要一定的開(kāi)銷(xiāo),性能問(wèn)題需要考慮。

二、基于緩存的分布式鎖

相比較于基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖的方案來(lái)說(shuō),基于緩存來(lái)實(shí)現(xiàn)在性能方面會(huì)表現(xiàn)的更好一點(diǎn)。

目前有很多成熟的緩存產(chǎn)品,包括Redis,memcached等。這里以Redis為例來(lái)分析下使用緩存實(shí)現(xiàn)分布式鎖的方案。

基于Redis實(shí)現(xiàn)分布式鎖在網(wǎng)上有很多相關(guān)文章,其中主要的實(shí)現(xiàn)方式是使用Jedis.setNX方法來(lái)實(shí)現(xiàn)。

public boolean trylock(String key) {  
  ResultCode code = jedis.setNX(key, "This is a Lock.");  
  if (ResultCode.SUCCESS.equals(code))    
    return true;  
  else    
    return false; 
} 
public boolean unlock(String key){
  ldbTairManager.invalid(NAMESPACE, key); 
}

以上實(shí)現(xiàn)方式同樣存在幾個(gè)問(wèn)題:

1、單點(diǎn)問(wèn)題。

2、這把鎖沒(méi)有失效時(shí)間,一旦解鎖操作失敗,就會(huì)導(dǎo)致鎖記錄一直在redis中,其他線程無(wú)法再獲得到鎖。

3、這把鎖只能是非阻塞的,無(wú)論成功還是失敗都直接返回。

4、這把鎖是非重入的,一個(gè)線程獲得鎖之后,在釋放鎖之前,無(wú)法再次獲得該鎖,因?yàn)槭褂玫降膋ey在redis中已經(jīng)存在。無(wú)法再執(zhí)行setNX操作。

5、這把鎖是非公平的,所有等待的線程同時(shí)去發(fā)起setNX操作,運(yùn)氣好的線程能獲取鎖。

當(dāng)然,同樣有方式可以解決。

  • 現(xiàn)在主流的緩存服務(wù)都支持集群部署,通過(guò)集群來(lái)解決單點(diǎn)問(wèn)題。
  • 沒(méi)有失效時(shí)間?redis的setExpire方法支持傳入失效時(shí)間,到達(dá)時(shí)間之后數(shù)據(jù)會(huì)自動(dòng)刪除。
  • 非阻塞?while重復(fù)執(zhí)行。
  • 非可重入?在一個(gè)線程獲取到鎖之后,把當(dāng)前主機(jī)信息和線程信息保存起來(lái),下次再獲取之前先檢查自己是不是當(dāng)前鎖的擁有者。
  • 非公平?在線程獲取鎖之前先把所有等待的線程放入一個(gè)隊(duì)列中,然后按先進(jìn)先出原則獲取鎖。

redis集群的同步策略是需要時(shí)間的,有可能A線程setNX成功后拿到鎖,但是這個(gè)值還沒(méi)有更新到B線程執(zhí)行setNX的這臺(tái)服務(wù)器,那就會(huì)產(chǎn)生并發(fā)問(wèn)題。

redis的作者Salvatore Sanfilippo,提出了Redlock算法,該算法實(shí)現(xiàn)了比單一節(jié)點(diǎn)更安全、可靠的分布式鎖管理(DLM)。

Redlock算法假設(shè)有N個(gè)redis節(jié)點(diǎn),這些節(jié)點(diǎn)互相獨(dú)立,一般設(shè)置為N=5,這N個(gè)節(jié)點(diǎn)運(yùn)行在不同的機(jī)器上以保持物理層面的獨(dú)立。

算法的步驟如下:

1、客戶端獲取當(dāng)前時(shí)間,以毫秒為單位。

2、客戶端嘗試獲取N個(gè)節(jié)點(diǎn)的鎖,(每個(gè)節(jié)點(diǎn)獲取鎖的方式和前面說(shuō)的緩存鎖一樣),N個(gè)節(jié)點(diǎn)以相同的key和value獲取鎖??蛻舳诵枰O(shè)置接口訪問(wèn)超時(shí),接口超時(shí)時(shí)間需要遠(yuǎn)遠(yuǎn)小于鎖超時(shí)時(shí)間,比如鎖自動(dòng)釋放的時(shí)間是10s,那么接口超時(shí)大概設(shè)置5-50ms。這樣可以在有redis節(jié)點(diǎn)宕機(jī)后,訪問(wèn)該節(jié)點(diǎn)時(shí)能盡快超時(shí),而減小鎖的正常使用。

3、客戶端計(jì)算在獲得鎖的時(shí)候花費(fèi)了多少時(shí)間,方法是用當(dāng)前時(shí)間減去在步驟一獲取的時(shí)間,只有客戶端獲得了超過(guò)3個(gè)節(jié)點(diǎn)的鎖,而且獲取鎖的時(shí)間小于鎖的超時(shí)時(shí)間,客戶端才獲得了分布式鎖。

4、客戶端獲取的鎖的時(shí)間為設(shè)置的鎖超時(shí)時(shí)間減去步驟三計(jì)算出的獲取鎖花費(fèi)時(shí)間。

5、如果客戶端獲取鎖失敗了,客戶端會(huì)依次刪除所有的鎖。

使用Redlock算法,可以保證在掛掉最多2個(gè)節(jié)點(diǎn)的時(shí)候,分布式鎖服務(wù)仍然能工作,這相比之前的數(shù)據(jù)庫(kù)鎖和緩存鎖大大提高了可用性,由于redis的高效性能,分布式緩存鎖性能并不比數(shù)據(jù)庫(kù)鎖差。但是,有一位分布式的專(zhuān)家寫(xiě)了一篇文章《How to do distributed locking》,質(zhì)疑Redlock的正確性。

該專(zhuān)家提到,考慮分布式鎖的時(shí)候需要考慮兩個(gè)方面:性能和正確性。

如果使用高性能的分布式鎖,對(duì)正確性要求不高的場(chǎng)景下,那么使用緩存鎖就足夠了。

如果使用可靠性高的分布式鎖,那么就需要考慮嚴(yán)格的可靠性問(wèn)題。而Redlock則不符合正確性。為什么不符合呢?專(zhuān)家列舉了幾個(gè)方面。

現(xiàn)在很多編程語(yǔ)言使用的虛擬機(jī)都有GC功能,在Full GC的時(shí)候,程序會(huì)停下來(lái)處理GC,有些時(shí)候Full GC耗時(shí)很長(zhǎng),甚至程序有幾分鐘的卡頓,文章列舉了HBase的例子,HBase有時(shí)候GC幾分鐘,會(huì)導(dǎo)致租約超時(shí)。而且Full GC什么時(shí)候到來(lái),程序無(wú)法掌控,程序的任何時(shí)候都可能停下來(lái)處理GC,比如下圖,客戶端1獲得了鎖,正準(zhǔn)備處理共享資源的時(shí)候,發(fā)生了Full GC直到鎖過(guò)期。這樣,客戶端2又獲得了鎖,開(kāi)始處理共享資源。在客戶端2處理的時(shí)候,客戶端1 Full GC完成,也開(kāi)始處理共享資源,這樣就出現(xiàn)了2個(gè)客戶端都在處理共享資源的情況。

淺談分布式鎖的幾種使用方式(redis、zookeeper、數(shù)據(jù)庫(kù))

專(zhuān)家給出了解決辦法,如下圖,看起來(lái)就是MVCC,給鎖帶上token,token就是version的概念,每次操作鎖完成,token都會(huì)加1,在處理共享資源的時(shí)候帶上token,只有指定版本的token能夠處理共享資源。

淺談分布式鎖的幾種使用方式(redis、zookeeper、數(shù)據(jù)庫(kù))

然后專(zhuān)家還說(shuō)到了算法依賴(lài)本地時(shí)間,而且redis在處理key過(guò)期的時(shí)候,依賴(lài)gettimeofday方法獲得時(shí)間,而不是monotonic clock,這也會(huì)帶來(lái)時(shí)間的不準(zhǔn)確。比如一下場(chǎng)景,兩個(gè)客戶端client 1和client 2,5個(gè)redis節(jié)點(diǎn)nodes (A, B, C, D and E)。

1、client 1從A、B、C成功獲取鎖,從D、E獲取鎖網(wǎng)絡(luò)超時(shí)。

2、節(jié)點(diǎn)C的時(shí)鐘不準(zhǔn)確,導(dǎo)致鎖超時(shí)。

3、client 2從C、D、E成功獲取鎖,從A、B獲取鎖網(wǎng)絡(luò)超時(shí)。

4、這樣client 1和client 2都獲得了鎖。

總結(jié)專(zhuān)家關(guān)于Redlock不可用的兩點(diǎn):

1、GC等場(chǎng)景可能隨時(shí)發(fā)生,并導(dǎo)致在客戶端獲取了鎖,在處理中超時(shí),導(dǎo)致另外的客戶端獲取了鎖。專(zhuān)家還給出了使用自增token的解決方法。

2、算法依賴(lài)本地時(shí)間,會(huì)出現(xiàn)時(shí)鐘不準(zhǔn),導(dǎo)致2個(gè)客戶端同時(shí)獲得鎖的情況。
所以專(zhuān)家給出的結(jié)論是,只有在有界的網(wǎng)絡(luò)延遲、有界的程序中斷、有界的時(shí)鐘錯(cuò)誤范圍,Redlock才能正常工作,但是這三種場(chǎng)景的邊界又是無(wú)法確認(rèn)的,所以專(zhuān)家不建議使用Redlock。對(duì)于正確性要求高的場(chǎng)景,專(zhuān)家推薦了Zookeeper,關(guān)于使用Zookeeper作為分布式鎖后面再討論。

Redis作者的回應(yīng)

redis作者看到這個(gè)專(zhuān)家的文章后,寫(xiě)了一篇博客予以回應(yīng)。作者很客氣的感謝了專(zhuān)家,然后表達(dá)出了對(duì)專(zhuān)家觀點(diǎn)的不認(rèn)同。

I asked for an analysis in the original Redlock specification here: http://redis.io/topics/distlock. So thank you Martin. However I don't agree with the analysis.

redis作者關(guān)于使用token解決鎖超時(shí)問(wèn)題可以概括成下面五點(diǎn):

觀點(diǎn)1,使用分布式鎖一般是在,你沒(méi)有其他方式去控制共享資源了,專(zhuān)家使用token來(lái)保證對(duì)共享資源的處理,那么就不需要分布式鎖了。

觀點(diǎn)2,對(duì)于token的生成,為保證不同客戶端獲得的token的可靠性,生成token的服務(wù)還是需要分布式鎖保證服務(wù)的可靠性。

觀點(diǎn)3,對(duì)于專(zhuān)家說(shuō)的自增的token的方式,redis作者認(rèn)為完全沒(méi)必要,每個(gè)客戶端可以生成唯一的uuid作為token,給共享資源設(shè)置為只有該uuid的客戶端才能處理的狀態(tài),這樣其他客戶端就無(wú)法處理該共享資源,直到獲得鎖的客戶端釋放鎖。

觀點(diǎn)4,redis作者認(rèn)為,對(duì)于token是有序的,并不能解決專(zhuān)家提出的GC問(wèn)題,如上圖所示,如果token 34的客戶端寫(xiě)入過(guò)程中發(fā)送GC導(dǎo)致鎖超時(shí),另外的客戶端可能獲得token 35的鎖,并再次開(kāi)始寫(xiě)入,導(dǎo)致鎖沖突。所以token的有序并不能跟共享資源結(jié)合起來(lái)。

觀點(diǎn)5,redis作者認(rèn)為,大部分場(chǎng)景下,分布式鎖用來(lái)處理非事務(wù)場(chǎng)景下的更新問(wèn)題。作者意思應(yīng)該是有些場(chǎng)景很難結(jié)合token處理共享資源,所以得依賴(lài)鎖去鎖定資源并進(jìn)行處理。

專(zhuān)家說(shuō)到的另一個(gè)時(shí)鐘問(wèn)題,redis作者也給出了解釋??蛻舳藢?shí)際獲得的鎖的時(shí)間是默認(rèn)的超時(shí)時(shí)間,減去獲取鎖所花費(fèi)的時(shí)間,如果獲取鎖花費(fèi)時(shí)間過(guò)長(zhǎng)導(dǎo)致超過(guò)了鎖的默認(rèn)超時(shí)間,那么此時(shí)客戶端并不能獲取到鎖,不會(huì)存在專(zhuān)家提出的例子。

個(gè)人感覺(jué)

第一個(gè)問(wèn)題我概括為,在一個(gè)客戶端獲取了分布式鎖后,在客戶端的處理過(guò)程中,可能出現(xiàn)鎖超時(shí)釋放的情況,這里說(shuō)的處理中除了GC等非抗力外,程序流程未處理完也是可能發(fā)生的。之前在說(shuō)到數(shù)據(jù)庫(kù)鎖設(shè)置的超時(shí)時(shí)間2分鐘,如果出現(xiàn)某個(gè)任務(wù)占用某個(gè)訂單鎖超過(guò)2分鐘,那么另一個(gè)交易中心就可以獲得這把訂單鎖,從而兩個(gè)交易中心同時(shí)處理同一個(gè)訂單。正常情況,任務(wù)當(dāng)然秒級(jí)處理完成,可是有時(shí)候,加入某個(gè)rpc請(qǐng)求設(shè)置的超時(shí)時(shí)間過(guò)長(zhǎng),一個(gè)任務(wù)中有多個(gè)這樣的超時(shí)請(qǐng)求,那么,很可能就出現(xiàn)超過(guò)自動(dòng)解鎖時(shí)間了。當(dāng)初我們的交易模塊是用C++寫(xiě)的,不存在GC,如果用java寫(xiě),中間還可能出現(xiàn)Full GC,那么鎖超時(shí)解鎖后,自己客戶端無(wú)法感知,是件非常嚴(yán)重的事情。我覺(jué)得這不是鎖本身的問(wèn)題,上面說(shuō)到的任何一個(gè)分布式鎖,只要自帶了超時(shí)釋放的特性,都會(huì)出現(xiàn)這樣的問(wèn)題。如果使用鎖的超時(shí)功能,那么客戶端一定得設(shè)置獲取鎖超時(shí)后,采取相應(yīng)的處理,而不是繼續(xù)處理共享資源。Redlock的算法,在客戶端獲取鎖后,會(huì)返回客戶端能占用的鎖時(shí)間,客戶端必須處理該時(shí)間,讓任務(wù)在超過(guò)該時(shí)間后停止下來(lái)。

第二個(gè)問(wèn)題,自然就是分布式專(zhuān)家沒(méi)有理解Redlock。Redlock有個(gè)關(guān)鍵的特性是,獲取鎖的時(shí)間是鎖默認(rèn)超時(shí)的總時(shí)間減去獲取鎖所花費(fèi)的時(shí)間,這樣客戶端處理的時(shí)間就是一個(gè)相對(duì)時(shí)間,就跟本地時(shí)間無(wú)關(guān)了。

由此看來(lái),Redlock的正確性是能得到很好的保證的。仔細(xì)分析Redlock,相比于一個(gè)節(jié)點(diǎn)的redis,Redlock提供的最主要的特性是可靠性更高,這在有些場(chǎng)景下是很重要的特性。但是我覺(jué)得Redlock為了實(shí)現(xiàn)可靠性,卻花費(fèi)了過(guò)大的代價(jià)。

首先必須部署5個(gè)節(jié)點(diǎn)才能讓Redlock的可靠性更強(qiáng)。

然后需要請(qǐng)求5個(gè)節(jié)點(diǎn)才能獲取到鎖,通過(guò)Future的方式,先并發(fā)向5個(gè)節(jié)點(diǎn)請(qǐng)求,再一起獲得響應(yīng)結(jié)果,能縮短響應(yīng)時(shí)間,不過(guò)還是比單節(jié)點(diǎn)redis鎖要耗費(fèi)更多時(shí)間。

然后由于必須獲取到5個(gè)節(jié)點(diǎn)中的3個(gè)以上,所以可能出現(xiàn)獲取鎖沖突,即大家都獲得了1-2把鎖,結(jié)果誰(shuí)也不能獲取到鎖,這個(gè)問(wèn)題,redis作者借鑒了raft算法的精髓,通過(guò)沖突后在隨機(jī)時(shí)間開(kāi)始,可以大大降低沖突時(shí)間,但是這問(wèn)題并不能很好的避免,特別是在第一次獲取鎖的時(shí)候,所以獲取鎖的時(shí)間成本增加了。

如果5個(gè)節(jié)點(diǎn)有2個(gè)宕機(jī),此時(shí)鎖的可用性會(huì)極大降低,首先必須等待這兩個(gè)宕機(jī)節(jié)點(diǎn)的結(jié)果超時(shí)才能返回,另外只有3個(gè)節(jié)點(diǎn),客戶端必須獲取到這全部3個(gè)節(jié)點(diǎn)的鎖才能擁有鎖,難度也加大了。

如果出現(xiàn)網(wǎng)絡(luò)分區(qū),那么可能出現(xiàn)客戶端永遠(yuǎn)也無(wú)法獲取鎖的情況。

分析了這么多原因,我覺(jué)得Redlock的問(wèn)題,最關(guān)鍵的一點(diǎn)在于Redlock需要客戶端去保證寫(xiě)入的一致性,后端5個(gè)節(jié)點(diǎn)完全獨(dú)立,所有的客戶端都得操作這5個(gè)節(jié)點(diǎn)。如果5個(gè)節(jié)點(diǎn)有一個(gè)leader,客戶端只要從leader獲取鎖,其他節(jié)點(diǎn)能同步leader的數(shù)據(jù),這樣,分區(qū)、超時(shí)、沖突等問(wèn)題都不會(huì)存在。所以為了保證分布式鎖的正確性,我覺(jué)得使用強(qiáng)一致性的分布式協(xié)調(diào)服務(wù)能更好的解決問(wèn)題。

問(wèn)題又來(lái)了,失效時(shí)間我設(shè)置多長(zhǎng)時(shí)間為好?如何設(shè)置的失效時(shí)間太短,方法沒(méi)等執(zhí)行完,鎖就自動(dòng)釋放了,那么就會(huì)產(chǎn)生并發(fā)問(wèn)題。如果設(shè)置的時(shí)間太長(zhǎng),其他獲取鎖的線程就可能要平白的多等一段時(shí)間。

這個(gè)問(wèn)題使用數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖同樣存在。

對(duì)于這個(gè)問(wèn)題目前主流的做法是每獲得一個(gè)鎖時(shí),只設(shè)置一個(gè)很短的超時(shí)時(shí)間,同時(shí)起一個(gè)線程在每次快要到超時(shí)時(shí)間時(shí)去刷新鎖的超時(shí)時(shí)間。在釋放鎖的同時(shí)結(jié)束這個(gè)線程。如redis官方的分布式鎖組件redisson,就是用的這種方案。

使用緩存實(shí)現(xiàn)分布式鎖的優(yōu)點(diǎn)

性能好。

使用緩存實(shí)現(xiàn)分布式鎖的缺點(diǎn)

實(shí)現(xiàn)過(guò)于負(fù)責(zé),需要考慮的因素太多。

基于Zookeeper實(shí)現(xiàn)的分布式鎖

基于zookeeper臨時(shí)有序節(jié)點(diǎn)可以實(shí)現(xiàn)的分布式鎖。

大致思想即為:每個(gè)客戶端對(duì)某個(gè)方法加鎖時(shí),在zookeeper上的與該方法對(duì)應(yīng)的指定節(jié)點(diǎn)的目錄下,生成一個(gè)唯一的瞬時(shí)有序節(jié)點(diǎn)。 判斷是否獲取鎖的方式很簡(jiǎn)單,只需要判斷有序節(jié)點(diǎn)中序號(hào)最小的一個(gè)。 當(dāng)釋放鎖的時(shí)候,只需將這個(gè)瞬時(shí)節(jié)點(diǎn)刪除即可。同時(shí),其可以避免服務(wù)宕機(jī)導(dǎo)致的鎖無(wú)法釋放,而產(chǎn)生的死鎖問(wèn)題。

來(lái)看下Zookeeper能不能解決前面提到的問(wèn)題。

  • 鎖無(wú)法釋放?使用Zookeeper可以有效的解決鎖無(wú)法釋放的問(wèn)題,因?yàn)樵趧?chuàng)建鎖的時(shí)候,客戶端會(huì)在ZK中創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn),一旦客戶端獲取到鎖之后突然掛掉(Session連接斷開(kāi)),那么這個(gè)臨時(shí)節(jié)點(diǎn)就會(huì)自動(dòng)刪除掉。其他客戶端就可以再次獲得鎖。
  • 非阻塞鎖?使用Zookeeper可以實(shí)現(xiàn)阻塞的鎖,客戶端可以通過(guò)在ZK中創(chuàng)建順序節(jié)點(diǎn),并且在節(jié)點(diǎn)上綁定監(jiān)聽(tīng)器,一旦節(jié)點(diǎn)有變化,Zookeeper會(huì)通知客戶端,客戶端可以檢查自己創(chuàng)建的節(jié)點(diǎn)是不是當(dāng)前所有節(jié)點(diǎn)中序號(hào)最小的,如果是,那么自己就獲取到鎖,便可以執(zhí)行業(yè)務(wù)邏輯了。
  • 不可重入?使用Zookeeper也可以有效的解決不可重入的問(wèn)題,客戶端在創(chuàng)建節(jié)點(diǎn)的時(shí)候,把當(dāng)前客戶端的主機(jī)信息和線程信息直接寫(xiě)入到節(jié)點(diǎn)中,下次想要獲取鎖的時(shí)候和當(dāng)前最小的節(jié)點(diǎn)中的數(shù)據(jù)比對(duì)一下就可以了。如果和自己的信息一樣,那么自己直接獲取到鎖,如果不一樣就再創(chuàng)建一個(gè)臨時(shí)的順序節(jié)點(diǎn),參與排隊(duì)。
  • 單點(diǎn)問(wèn)題?使用Zookeeper可以有效的解決單點(diǎn)問(wèn)題,ZK是集群部署的,只要集群中有半數(shù)以上的機(jī)器存活,就可以對(duì)外提供服務(wù)。
  • 公平問(wèn)題?使用Zookeeper可以解決公平鎖問(wèn)題,客戶端在ZK中創(chuàng)建的臨時(shí)節(jié)點(diǎn)是有序的,每次鎖被釋放時(shí),ZK可以通知最小節(jié)點(diǎn)來(lái)獲取鎖,保證了公平。

問(wèn)題又來(lái)了,我們知道Zookeeper需要集群部署,會(huì)不會(huì)出現(xiàn)Redis集群那樣的數(shù)據(jù)同步問(wèn)題呢?

Zookeeper是一個(gè)保證了弱一致性即最終一致性的分布式組件。

Zookeeper采用稱(chēng)為Quorum Based Protocol的數(shù)據(jù)同步協(xié)議。假如Zookeeper集群有N臺(tái)Zookeeper服務(wù)器(N通常取奇數(shù),3臺(tái)能夠滿足數(shù)據(jù)可靠性同時(shí)有很高讀寫(xiě)性能,5臺(tái)在數(shù)據(jù)可靠性和讀寫(xiě)性能方面平衡最好),那么用戶的一個(gè)寫(xiě)操作,首先同步到N/2 + 1臺(tái)服務(wù)器上,然后返回給用戶,提示用戶寫(xiě)成功?;赒uorum Based Protocol的數(shù)據(jù)同步協(xié)議決定了Zookeeper能夠支持什么強(qiáng)度的一致性。

在分布式環(huán)境下,滿足強(qiáng)一致性的數(shù)據(jù)儲(chǔ)存基本不存在,它要求在更新一個(gè)節(jié)點(diǎn)的數(shù)據(jù),需要同步更新所有的節(jié)點(diǎn)。這種同步策略出現(xiàn)在主從同步復(fù)制的數(shù)據(jù)庫(kù)中。但是這種同步策略,對(duì)寫(xiě)性能的影響太大而很少見(jiàn)于實(shí)踐。因?yàn)閆ookeeper是同步寫(xiě)N/2+1個(gè)節(jié)點(diǎn),還有N/2個(gè)節(jié)點(diǎn)沒(méi)有同步更新,所以Zookeeper不是強(qiáng)一致性的。

用戶的數(shù)據(jù)更新操作,不保證后續(xù)的讀操作能夠讀到更新后的值,但是最終會(huì)呈現(xiàn)一致性。犧牲一致性,并不是完全不管數(shù)據(jù)的一致性,否則數(shù)據(jù)是混亂的,那么系統(tǒng)可用性再高分布式再好也沒(méi)有了價(jià)值。犧牲一致性,只是不再要求關(guān)系型數(shù)據(jù)庫(kù)中的強(qiáng)一致性,而是只要系統(tǒng)能達(dá)到最終一致性即可。

Zookeeper是否滿足因果一致性,需要看客戶端的編程方式。

  • 不滿足因果一致性的做法
  • A進(jìn)程向Zookeeper的/z寫(xiě)入一個(gè)數(shù)據(jù),成功返回
  • A進(jìn)程通知B進(jìn)程,A已經(jīng)修改了/z的數(shù)據(jù)
  • B讀取Zookeeper的/z的數(shù)據(jù)
  • 由于B連接的Zookeeper的服務(wù)器有可能還沒(méi)有得到A寫(xiě)入數(shù)據(jù)的更新,那么B將讀不到A寫(xiě)入的數(shù)據(jù)

滿足因果一致性的做法

  • B進(jìn)程監(jiān)聽(tīng)Zookeeper上/z的數(shù)據(jù)變化
  • A進(jìn)程向Zookeeper的/z寫(xiě)入一個(gè)數(shù)據(jù),成功返回前,Zookeeper需要調(diào)用注冊(cè)在/z上的監(jiān)聽(tīng)器,Leader將數(shù)據(jù)變化的通知告訴B
  • B進(jìn)程的事件響應(yīng)方法得到響應(yīng)后,去取變化的數(shù)據(jù),那么B一定能夠得到變化的值
  • 這里的因果一致性提現(xiàn)在Leader和B之間的因果一致性,也就是是Leader通知了數(shù)據(jù)有變化

第二種事件監(jiān)聽(tīng)機(jī)制也是對(duì)Zookeeper進(jìn)行正確編程應(yīng)該使用的方法,所以,Zookeeper應(yīng)該是滿足因果一致性的

所以我們?cè)诨赯ookeeper實(shí)現(xiàn)分布式鎖的時(shí)候,應(yīng)該使用滿足因果一致性的做法,即等待鎖的線程都監(jiān)聽(tīng)Zookeeper上鎖的變化,在鎖被釋放的時(shí)候,Zookeeper會(huì)將鎖變化的通知告訴滿足公平鎖條件的等待線程。

可以直接使用zookeeper第三方庫(kù)客戶端,這個(gè)客戶端中封裝了一個(gè)可重入的鎖服務(wù)。

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; 
}

使用ZK實(shí)現(xiàn)的分布式鎖好像完全符合了本文開(kāi)頭我們對(duì)一個(gè)分布式鎖的所有期望。但是,其實(shí)并不是,Zookeeper實(shí)現(xiàn)的分布式鎖其實(shí)存在一個(gè)缺點(diǎn),那就是性能上可能并沒(méi)有緩存服務(wù)那么高。因?yàn)槊看卧趧?chuàng)建鎖和釋放鎖的過(guò)程中,都要?jiǎng)討B(tài)創(chuàng)建、銷(xiāo)毀瞬時(shí)節(jié)點(diǎn)來(lái)實(shí)現(xiàn)鎖功能。ZK中創(chuàng)建和刪除節(jié)點(diǎn)只能通過(guò)Leader服務(wù)器來(lái)執(zhí)行,然后將數(shù)據(jù)同不到所有的Follower機(jī)器上。

使用Zookeeper實(shí)現(xiàn)分布式鎖的優(yōu)點(diǎn)

有效的解決單點(diǎn)問(wèn)題,不可重入問(wèn)題,非阻塞問(wèn)題以及鎖無(wú)法釋放的問(wèn)題。實(shí)現(xiàn)起來(lái)較為簡(jiǎn)單。

使用Zookeeper實(shí)現(xiàn)分布式鎖的缺點(diǎn)

性能上不如使用緩存實(shí)現(xiàn)分布式鎖。 需要對(duì)ZK的原理有所了解。

三種方案的比較從理解的難易程度角度(從低到高)

數(shù)據(jù)庫(kù) > 緩存 > Zookeeper

從實(shí)現(xiàn)的復(fù)雜性角度(從低到高)

Zookeeper > 緩存 > 數(shù)據(jù)庫(kù)

從性能角度(從高到低)

緩存 > Zookeeper >= 數(shù)據(jù)庫(kù)

從可靠性角度(從高到低)

Zookeeper > 緩存 > 數(shù)據(jù)庫(kù)\

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI