一文理解分布式鎖的實(shí)現(xiàn)方式
分布式鎖的應(yīng)用場(chǎng)景
分布式鎖的應(yīng)用場(chǎng)景主要包括兩類(lèi):
處理效率提升:減少重復(fù)任務(wù)的執(zhí)行,避免資源處理效率的浪費(fèi)(例如冪等場(chǎng)景)。
數(shù)據(jù)準(zhǔn)確性保障:在數(shù)據(jù)資源的并發(fā)訪(fǎng)問(wèn)時(shí),避免數(shù)據(jù)不一致情況,甚至數(shù)據(jù)更新?lián)p失等。

分布式鎖的設(shè)計(jì)要求
分布式鎖需要是一把可重入鎖(避免死鎖)。
分布式鎖最好是一把阻塞鎖(沒(méi)有獲得鎖的線(xiàn)程不是直接返回,而是在阻塞狀態(tài))。
分布式鎖最好是一把公平鎖,防止過(guò)度饑餓。
分布式鎖有高可用的獲取鎖和釋放鎖功能。
分布式鎖的實(shí)現(xiàn)方式
分布式鎖一般有四種實(shí)現(xiàn)方式:
基于數(shù)據(jù)庫(kù)。
基于Redis的分布式鎖。
基于ZooKeeper的分布式鎖。
基于etcd的分布式鎖。
數(shù)據(jù)庫(kù)
基于數(shù)據(jù)庫(kù)表
要實(shí)現(xiàn)分布式鎖,最簡(jiǎn)單的方式就是直接創(chuàng)建一張鎖表,然后通過(guò)操作該表中的數(shù)據(jù)來(lái)實(shí)現(xiàn)鎖。
當(dāng)要鎖住某個(gè)方法或資源時(shí),就在該表中增加一條記錄,想要釋放鎖的時(shí)候就刪除這條記錄。
創(chuàng)建這樣一張數(shù)據(jù)庫(kù)表:
CREATE TABLE `method_lock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的方法名',
`desc` varchar(1024) NOT NULL DEFAULT '備注信息(函數(shù)參數(shù)等信息)',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存數(shù)據(jù)時(shí)間,自動(dòng)生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_method_name_desc` (`method_name `,`desc`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
當(dāng)想要鎖住某個(gè)方法時(shí),執(zhí)行以下SQL:
insert into method_lock(method_name,desc) values (`method_name`,`desc`)
因?yàn)閷?duì)method_name和desc做了唯一性約束,如果有多個(gè)請(qǐng)求同時(shí)提交到數(shù)據(jù)庫(kù)的話(huà),數(shù)據(jù)庫(kù)會(huì)保證同一個(gè)資源只有一個(gè)操作可以成功,那么就可以認(rèn)為操作成功的那個(gè)線(xiàn)程獲得了該方法的鎖,可以繼續(xù)執(zhí)行業(yè)務(wù)邏輯。
當(dāng)方法執(zhí)行完畢之后,想要釋放鎖的話(huà),需要執(zhí)行以下Sql:
delete from method_lock where method_name ='xxx' and desc='xxxx'
這種實(shí)現(xiàn)方式存在的問(wèn)題:
鎖強(qiáng)依賴(lài)數(shù)據(jù)庫(kù)的可用性,數(shù)據(jù)庫(kù)是單點(diǎn),一旦數(shù)據(jù)庫(kù)掛掉,會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)不可用。
鎖沒(méi)有失效時(shí)間,一旦解鎖操作失敗,就會(huì)導(dǎo)致鎖記錄一直在數(shù)據(jù)庫(kù)中,其他線(xiàn)程無(wú)法再獲得到鎖。
鎖只能是非阻塞的,因?yàn)閿?shù)據(jù)的insert操作,一旦插入失敗就會(huì)直接報(bào)錯(cuò)。沒(méi)有獲得鎖的線(xiàn)程并不會(huì)進(jìn)入排隊(duì)隊(duì)列,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作。
這把鎖是非重入的,同一個(gè)線(xiàn)程在沒(méi)有釋放鎖之前無(wú)法再次獲得該鎖。因?yàn)閿?shù)據(jù)中數(shù)據(jù)已經(jīng)存在。
相應(yīng)的解決方法:
同時(shí)部署兩個(gè)數(shù)據(jù)庫(kù),一臺(tái)業(yè)務(wù)用,另一臺(tái)做熱備。
設(shè)置一個(gè)定時(shí)任務(wù),每隔一定時(shí)間把數(shù)據(jù)庫(kù)中的超時(shí)鎖清理掉。
使用while重復(fù)執(zhí)行。同時(shí)需要設(shè)置重試次數(shù),防止持續(xù)拿不到鎖導(dǎo)致服務(wù)器資源耗盡。
在表中加個(gè)字段,記錄當(dāng)前獲得鎖的機(jī)器的主機(jī)信息和線(xiàn)程信息,那么下次再獲取鎖的時(shí)候先查詢(xún)數(shù)據(jù)庫(kù),如果主機(jī)信息和線(xiàn)程信息與表中的信息吻合,直接把鎖分配給該線(xiàn)程。
基于數(shù)據(jù)庫(kù)排他鎖
除了基于數(shù)據(jù)庫(kù)表,還可以借助數(shù)據(jù)庫(kù)的排他鎖來(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);
/**
* 設(shè)置重試次數(shù),防止持續(xù)拿不到鎖導(dǎo)致服務(wù)器資源耗盡
*/
int count = 0;
while (count < 4) {
try {
result = select * from method_lock where method_name = xxxx and desc = xxx for update;
if (result == null) {
return true;
}
} catch (Exception e) {
}
//為空或者拋異常的話(huà)都表示沒(méi)有獲取到鎖
sleep(1000);
count++;
}
return false;
}
獲得排它鎖的線(xiàn)程即獲得分布式鎖。當(dāng)獲取到鎖之后,可以繼續(xù)執(zhí)行業(yè)務(wù)邏輯,執(zhí)行完方法之后,再通過(guò)以下方法解鎖:
public void unlock(){
connection.commit();
}
針對(duì)加鎖之后服務(wù)宕機(jī),無(wú)法釋放的問(wèn)題,使用這種方式,服務(wù)宕機(jī)之后數(shù)據(jù)庫(kù)會(huì)自己把鎖釋放掉。
但還是無(wú)法解決數(shù)據(jù)庫(kù)單點(diǎn)和可重入問(wèn)題。
此外,要使用排他鎖來(lái)進(jìn)行分布式鎖的lock,那么一個(gè)排他鎖長(zhǎng)時(shí)間不提交,就會(huì)占用數(shù)據(jù)庫(kù)連接。一旦類(lèi)似的連接變得多了,就可能把數(shù)據(jù)庫(kù)連接池?fù)伪?/p>
在查詢(xún)語(yǔ)句后面增加for update,數(shù)據(jù)庫(kù)會(huì)在查詢(xún)過(guò)程中給數(shù)據(jù)庫(kù)表增加排他鎖。但需要注意的是MySQL會(huì)對(duì)查詢(xún)進(jìn)行優(yōu)化,即便在條件中使用了索引字段,但是否使用索引來(lái)檢索數(shù)據(jù)是由MySQL通過(guò)判斷不同執(zhí)行計(jì)劃的代價(jià)來(lái)決定的:如果MySQL認(rèn)為全表掃效率更高,比如對(duì)一些數(shù)據(jù)量小的表,MySQL就不會(huì)使用索引。這種情況下查詢(xún)將出現(xiàn)表鎖,而不是行鎖,這會(huì)導(dǎo)致所有sql寫(xiě)操作阻塞。。。
基于數(shù)據(jù)庫(kù)樂(lè)觀鎖
前面基于數(shù)據(jù)庫(kù)悲觀鎖實(shí)現(xiàn)的分布式鎖,基于數(shù)據(jù)庫(kù)也可以使用樂(lè)觀鎖來(lái)實(shí)現(xiàn)分布式鎖(資源表增加version字段)。
先執(zhí)行SELECT操作查詢(xún)當(dāng)前數(shù)據(jù)的數(shù)據(jù)版本號(hào),比如當(dāng)前數(shù)據(jù)版本號(hào)是30:
SELECT id,version FROM method_lock WHERE method_name ='xxx' and desc='xxxx';執(zhí)行更新操作:
UPDATE method_lock SET version=27, update_time=NOW() WHERE id=xx AND version=30;如果上述UPDATE語(yǔ)句更新影響到了一行數(shù)據(jù),那就說(shuō)明搶占鎖成功。如果沒(méi)有更新影響到一行數(shù)據(jù),則說(shuō)明這個(gè)資源已經(jīng)被別人占位了。
基于數(shù)據(jù)庫(kù)表做樂(lè)觀鎖的缺點(diǎn):
原本一次的UPDATE操作,變?yōu)?次操作:SELECT版本號(hào)一次和UPDATE一次。增加了數(shù)據(jù)庫(kù)操作的次數(shù)。
如果業(yè)務(wù)場(chǎng)景中的一次業(yè)務(wù)流程中,多個(gè)資源都需要用保證數(shù)據(jù)一致性,那么如果全部使用基于數(shù)據(jù)庫(kù)資源表的樂(lè)觀鎖,就要讓每個(gè)資源都有一張資源表,這個(gè)在實(shí)際使用場(chǎng)景中肯定是無(wú)法滿(mǎn)足的。而且這些都基于數(shù)據(jù)庫(kù)操作,在高并發(fā)的要求下,對(duì)數(shù)據(jù)庫(kù)連接的開(kāi)銷(xiāo)一定是無(wú)法忍受的。
總結(jié)
數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖的優(yōu)點(diǎn):
直接使用數(shù)據(jù)庫(kù),使用簡(jiǎn)單且節(jié)省運(yùn)維成本。
數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖的缺點(diǎn):
系統(tǒng)不穩(wěn)定會(huì)造成各種各樣的問(wèn)題,在解決問(wèn)題的過(guò)程中會(huì)使整個(gè)方案變得越來(lái)越復(fù)雜。
操作數(shù)據(jù)庫(kù)需要一定的開(kāi)銷(xiāo)且長(zhǎng)時(shí)間不commit或者長(zhǎng)時(shí)間輪詢(xún),可能會(huì)占用較多連接資源。
數(shù)據(jù)庫(kù)的行級(jí)鎖在數(shù)據(jù)量小等情況時(shí)有可能轉(zhuǎn)變表級(jí)鎖,增加鎖表風(fēng)險(xiǎn)。
對(duì)于主從復(fù)制且讀寫(xiě)分離的數(shù)據(jù)庫(kù)來(lái)說(shuō),如果select操作在從庫(kù),而update在主庫(kù)的話(huà),如果存在主從延遲,可能會(huì)出現(xiàn)加鎖錯(cuò)誤問(wèn)題。建議將select與update放在同一個(gè)事務(wù)中,這樣就可以都在主庫(kù)進(jìn)行select和update了。
基于Redis的分布式鎖
基于Redis實(shí)現(xiàn)分布式鎖主要有兩大類(lèi),一類(lèi)是基于單機(jī),另一類(lèi)是基于Redis多機(jī)。
基于Redis單機(jī)實(shí)現(xiàn)的分布式鎖
加鎖命令基于Redis命令:SET key value NX EX max-lock-time。

加鎖與解鎖的代碼示例:
class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final Long RELEASE_SUCCESS = 1L;
/**
* 嘗試獲取分布式鎖
*
* @param jedis Redis客戶(hù)端
* @param lockKey 鎖
* @param requestId 請(qǐng)求標(biāo)識(shí) 自定義的隨機(jī)字符串,用于解鎖的時(shí)候判斷是否是當(dāng)前用戶(hù)加鎖
* @param expireTime 超期時(shí)間
* @return 是否獲取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
/**
* 釋放分布式鎖
* 獲取鎖對(duì)應(yīng)的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)
* @param jedis Redis客戶(hù)端
* @param lockKey 鎖
* @param requestId 請(qǐng)求標(biāo)識(shí)
* @return 是否釋放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
這種實(shí)現(xiàn)方式存在的問(wèn)題:
這把鎖只能是非阻塞的,無(wú)論成功還是失敗都直接返回。
這把鎖是非重入的,一個(gè)線(xiàn)程獲得鎖之后,在釋放鎖之前,無(wú)法再次獲得該鎖,因?yàn)槭褂玫降膋ey在Redis中已經(jīng)存在。
分布式鎖過(guò)期,而業(yè)務(wù)邏輯沒(méi)執(zhí)行完。
相應(yīng)的解決方法:
使用while重復(fù)執(zhí)行并設(shè)置重試次數(shù),防止持續(xù)拿不到鎖導(dǎo)致服務(wù)器資源耗盡。
在一個(gè)線(xiàn)程獲取到鎖之后,把當(dāng)前主機(jī)信息和線(xiàn)程信息保存起來(lái),下次再獲取之前先檢查自己是不是當(dāng)前鎖的擁有者。
自行維護(hù)續(xù)期邏輯。
使用Redisson的分布式鎖
目前互聯(lián)網(wǎng)公司在生產(chǎn)環(huán)境用的比較廣泛的開(kāi)源框架Redisson很好地解決了鎖被提前釋放這個(gè)問(wèn)題,非常的簡(jiǎn)便易用,且支持Redis單實(shí)例、Redis主從、Redis Sentinel、Redis Cluster等多種部署架構(gòu)。
Redisson框架會(huì)開(kāi)啟一個(gè)定時(shí)器的守護(hù)線(xiàn)程,每expireTime/3執(zhí)行一次,去檢查該線(xiàn)程的鎖是否存在,如果存在則對(duì)鎖的過(guò)期時(shí)間重新設(shè)置為expireTime,即利用守護(hù)線(xiàn)程對(duì)鎖進(jìn)行“續(xù)命”,防止鎖由于過(guò)期提前釋放。
其實(shí)現(xiàn)原理如圖所示:

這種實(shí)現(xiàn)方式存在的問(wèn)題和解決方法:
Redission的分布式鎖也是非阻塞的,同樣需要while重復(fù)執(zhí)行。
Redission的分布式鎖是可重入的。因?yàn)镽edisson的鎖是hset結(jié)構(gòu),key值就是客戶(hù)端的身份標(biāo)識(shí),value是加鎖次數(shù),從而實(shí)現(xiàn)了可重入加鎖。
基于Redis多機(jī)實(shí)現(xiàn)的分布式鎖Redlock
以上兩種基于Redis單機(jī)實(shí)現(xiàn)的分布式鎖都存在一個(gè)問(wèn)題:加鎖時(shí)只作用在一個(gè)Redis節(jié)點(diǎn)上,即使Redis通過(guò)Sentinel或者Cluster保證了高可用,但由于Redis的復(fù)制是異步的,Master節(jié)點(diǎn)獲取到鎖后在未完成數(shù)據(jù)同步的情況下發(fā)生故障轉(zhuǎn)移,此時(shí)其他客戶(hù)端上的線(xiàn)程依然可以獲取到鎖,因此會(huì)喪失鎖的安全性。
正因?yàn)槿绱耍琑edis的作者antirez提供了RedLock的算法來(lái)實(shí)現(xiàn)一個(gè)分布式鎖。該算法流程是這樣的:
假設(shè)有 N(N>=5)個(gè)Redis節(jié)點(diǎn),這些節(jié)點(diǎn)完全互相獨(dú)立。(不存在主從復(fù)制或者其他集群協(xié)調(diào)機(jī)制,確保這N個(gè)節(jié)點(diǎn)使用與在Redis單實(shí)例下相同的方法獲取和釋放鎖)
獲取鎖的過(guò)程,客戶(hù)端應(yīng)執(zhí)行如下操作:
獲取當(dāng)前Unix時(shí)間,以毫秒為單位。
依次嘗試從5個(gè)實(shí)例,使用相同的key和具有唯一性的value(例如UUID)獲取鎖。當(dāng)向Redis請(qǐng)求獲取鎖時(shí),客戶(hù)端應(yīng)該設(shè)置一個(gè)創(chuàng)建鎖的超時(shí)時(shí)間,這個(gè)超時(shí)時(shí)間應(yīng)該遠(yuǎn)小于鎖的失效時(shí)間。這樣可以避免服務(wù)器端Redis已經(jīng)掛掉的情況下,客戶(hù)端還在一直等待響應(yīng)結(jié)果。如果服務(wù)器端沒(méi)有在規(guī)定時(shí)間內(nèi)響應(yīng),客戶(hù)端應(yīng)該盡快嘗試去另外一個(gè)Redis實(shí)例請(qǐng)求獲取鎖。
客戶(hù)端使用當(dāng)前時(shí)間減去開(kāi)始獲取鎖時(shí)間(步驟1記錄的時(shí)間)就得到獲取鎖消耗的時(shí)間。當(dāng)且僅當(dāng)從大多數(shù)(大于N/2+1,這里是3個(gè)節(jié)點(diǎn))的Redis節(jié)點(diǎn)都取到鎖,并且獲取鎖消耗的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功。
如果取到了鎖,key的真正有效時(shí)間等于有效時(shí)間減去獲取鎖消耗的時(shí)間(步驟3計(jì)算的結(jié)果)。
如果因?yàn)槟承┰颍@取鎖失敗(沒(méi)有在至少N/2+1個(gè)Redis實(shí)例取到鎖或者取鎖時(shí)間已經(jīng)超過(guò)了有效時(shí)間),客戶(hù)端應(yīng)該在所有的Redis實(shí)例上進(jìn)行解鎖。(雖然某些Redis實(shí)例根本就沒(méi)有加鎖成功,防止某些節(jié)點(diǎn)獲取到鎖但是客戶(hù)端沒(méi)有得到響應(yīng)而導(dǎo)致接下來(lái)的一段時(shí)間不能被重新獲取鎖)
而分布式系統(tǒng)專(zhuān)家Martin針對(duì)Redlock提出了一個(gè)場(chǎng)景:假設(shè)多節(jié)點(diǎn)Redis系統(tǒng)有五個(gè)節(jié)點(diǎn)A/B/C/D/E和兩個(gè)客戶(hù)端C1和C2,如果其中一個(gè)Redis節(jié)點(diǎn)上的時(shí)鐘向前跳躍會(huì)發(fā)生什么?
客戶(hù)端C1獲得了對(duì)節(jié)點(diǎn)A、B、c的鎖定,由于網(wǎng)絡(luò)問(wèn)題,法到達(dá)節(jié)點(diǎn)D和節(jié)點(diǎn)E。
節(jié)點(diǎn)C上的時(shí)鐘向前跳,導(dǎo)致鎖提前過(guò)期。
客戶(hù)端C2在節(jié)點(diǎn)C、D、E上獲得鎖定,由于網(wǎng)絡(luò)問(wèn)題,無(wú)法到達(dá)A和B。
客戶(hù)端C1和客戶(hù)端C2現(xiàn)在都認(rèn)為他們自己持有鎖。
這說(shuō)明時(shí)鐘跳躍對(duì)于Redlock算法影響較大,這種情況一旦發(fā)生,Redlock是沒(méi)法正常工作的。
對(duì)此,Antirez指出Redlock算法對(duì)系統(tǒng)時(shí)鐘的要求并不需要完全精確,只要誤差不超過(guò)一定范圍不會(huì)產(chǎn)生影響,在實(shí)際環(huán)境中是完全合理的,通過(guò)恰當(dāng)?shù)倪\(yùn)維完全可以避免時(shí)鐘發(fā)生大的跳動(dòng)。
更多有關(guān)著Martin對(duì)Redlock算法的質(zhì)疑以及Antirez的回應(yīng),請(qǐng)查閱參考文檔3和4,感興趣的同學(xué)可以閱讀一下。同時(shí)也可以看參考文檔5和6鐵蕾大神的文章,更加快捷了解這場(chǎng)爭(zhēng)論。
總結(jié)
使用Redis實(shí)現(xiàn)分布式鎖的優(yōu)點(diǎn):
相比數(shù)據(jù)庫(kù)來(lái),Redis實(shí)現(xiàn)分布式鎖,可以提供更好的性能。
使用Redis實(shí)現(xiàn)分布式鎖的缺點(diǎn):
如果應(yīng)用場(chǎng)景是為了處理效率提升,協(xié)調(diào)各個(gè)客戶(hù)端避免做重復(fù)的工作,即使鎖失效了,發(fā)生業(yè)務(wù)邏輯重復(fù)執(zhí)行也不會(huì)有大的影響,則可以使用Redis實(shí)現(xiàn)分布式鎖。但是如果你的應(yīng)用場(chǎng)景是為了數(shù)據(jù)準(zhǔn)確性保障,那么用Redis實(shí)現(xiàn)分布式鎖并不合適(因?yàn)镽edis集群是AP模型)。為了正確性,需要考慮接口冪等性,同時(shí)使用zab(Zookeeper)、raft(etcd)等共識(shí)算法的中間件來(lái)實(shí)現(xiàn)嚴(yán)格意義上的分布式鎖。
補(bǔ)充
從Redis 2.6.12版本開(kāi)始,SET命令的行為可以通過(guò)一系列參數(shù)來(lái)修改:
EX seconds:將鍵的過(guò)期時(shí)間設(shè)置為seconds秒。執(zhí)行
SET key value EX seconds的效果等同于執(zhí)行SETEX key seconds value。PX milliseconds:將鍵的過(guò)期時(shí)間設(shè)置為 milliseconds 毫秒。執(zhí)行
SET key value PX milliseconds的效果等同于執(zhí)行PSETEX key milliseconds value。NX:只在鍵不存在時(shí),才對(duì)鍵進(jìn)行設(shè)置操作。執(zhí)行
SET key value NX的效果等同于執(zhí)行SETNX key value。XX:只在鍵已經(jīng)存在時(shí),才對(duì)鍵進(jìn)行設(shè)置操作。
因?yàn)镾ET命令可以通過(guò)參數(shù)來(lái)實(shí)現(xiàn)SETNX、SETEX以及PSETEX命令的效果,所以Redis將來(lái)的版本可能會(huì)廢棄并移除SETNX、SETEX和PSETEX這三個(gè)命令。
基于Zookeeper實(shí)現(xiàn)分布式鎖
ZooKeeper是一個(gè)分布式協(xié)調(diào)服務(wù)的開(kāi)源框架。主要用來(lái)解決分布式集群中應(yīng)用系統(tǒng)的一致性的問(wèn)題。
ZooKeeper本質(zhì)上是一個(gè)分布式的小文件存儲(chǔ)系統(tǒng)。提供基于類(lèi)似于文件系統(tǒng)的目錄樹(shù)方式的數(shù)據(jù)存儲(chǔ),并且可以對(duì)樹(shù)的節(jié)點(diǎn)進(jìn)行有效管理。

使用ZooKeeper實(shí)現(xiàn)分布式鎖的過(guò)程:
客戶(hù)端連接ZooKeeper,并在/tmp下創(chuàng)建臨時(shí)且有序的子節(jié)點(diǎn),第一個(gè)客戶(hù)端對(duì)應(yīng)的子節(jié)點(diǎn)為lock-0000,第二個(gè)為lock-0001,以此類(lèi)推。
客戶(hù)端獲取/lock下的子節(jié)點(diǎn)列表,判斷創(chuàng)建的節(jié)點(diǎn)是否為當(dāng)前子節(jié)點(diǎn)列表中序號(hào)最小的節(jié)點(diǎn),如果是則認(rèn)為獲得鎖,否則監(jiān)聽(tīng)前一個(gè)子節(jié)點(diǎn)的刪除消息。
獲取鎖后,執(zhí)行業(yè)務(wù)代碼流程,刪除當(dāng)前客戶(hù)端對(duì)應(yīng)的子節(jié)點(diǎn),鎖釋放。
例如:/tmp下的子節(jié)點(diǎn)列表為:lock-0000、lock-0001、lock-0002,序號(hào)為1的客戶(hù)端監(jiān)聽(tīng)序號(hào)為0000子節(jié)點(diǎn)的刪除消息,序號(hào)為2的監(jiān)聽(tīng)序號(hào)為0001子節(jié)點(diǎn)的刪除消息(業(yè)務(wù)代碼執(zhí)行完結(jié)束后刪除子節(jié)點(diǎn))。
這種實(shí)現(xiàn)方式存在的問(wèn)題和解決方法:
針對(duì)分布式鎖無(wú)法自動(dòng)釋放的問(wèn)題,Zookeeper可以有效地解決。因?yàn)樵趧?chuàng)建鎖的時(shí)候,客戶(hù)端會(huì)在ZK中創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn),一旦客戶(hù)端獲取到鎖之后突然掛掉(Session連接斷開(kāi)),那么這個(gè)臨時(shí)節(jié)點(diǎn)就會(huì)自動(dòng)刪除掉。其他客戶(hù)端就可以再次獲得鎖。
針對(duì)分布式鎖最好是阻塞鎖的問(wèn)題,Zookeeper通過(guò)在節(jié)點(diǎn)上綁定監(jiān)聽(tīng)器,當(dāng)獲取到鎖的時(shí)候,調(diào)用回調(diào)函數(shù)的方式,實(shí)現(xiàn)了阻塞鎖的效果。
針對(duì)分布式鎖的可重入特性,Zookeeper可以有效地解決。客戶(hù)端在創(chuàng)建節(jié)點(diǎn)的時(shí)候,把當(dāng)前客戶(hù)端的主機(jī)信息和線(xiàn)程信息直接寫(xiě)入到節(jié)點(diǎn)中,下次想要獲取鎖的時(shí)候和當(dāng)前最小的節(jié)點(diǎn)中的數(shù)據(jù)比對(duì)一下就可以了。如果和自己的信息一樣,那么自己直接獲取到鎖,如果不一樣就再創(chuàng)建一個(gè)臨時(shí)的順序節(jié)點(diǎn),參與排隊(duì)。
針對(duì)分布式單點(diǎn)問(wèn)題問(wèn)題導(dǎo)致的鎖失效問(wèn)題,Zookeeper可以有效地解決。Zookeeper是集群部署,只要集群中有半數(shù)以上的機(jī)器存活,就可以對(duì)外提供服務(wù)。
Apache Curator是一個(gè)Zookeeper的開(kāi)源客戶(hù)端,它提供了Zookeeper各種應(yīng)用場(chǎng)景(如共享鎖服務(wù)、master選舉、分布式計(jì)數(shù)器等)的抽象封裝,簡(jiǎn)化了ZooKeeper的操作。
總結(jié)
使用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):
因?yàn)閆ookeeper集群采用zab一致性協(xié)議,所以高并發(fā)場(chǎng)景,性能上不如使用Redis實(shí)現(xiàn)分布式鎖。
基于etcd的分布式鎖
有關(guān)etcd的機(jī)制以及分布式鎖的實(shí)現(xiàn),等小輝寫(xiě)到Service Mesh、K8s和etcd的時(shí)候,再將這一塊空缺填上。
分布式鎖的技術(shù)選型
目前以小輝的了解,生產(chǎn)環(huán)境應(yīng)該很少使用數(shù)據(jù)庫(kù)來(lái)做分布式鎖,即使基于數(shù)據(jù)庫(kù)的分布式鎖實(shí)現(xiàn)比較簡(jiǎn)單。
目前比較熱門(mén)的技術(shù)選型有基于Redis、Zookeeper和etcd的分布式鎖。其中基于Redis單機(jī)和Redisson的分布式鎖,都屬于AP模型;而基于Zookeeper與etcd的分布式鎖屬于CP模型。
在CAP理論中,由于分布式系統(tǒng)中多節(jié)點(diǎn)通信不可避免出現(xiàn)網(wǎng)絡(luò)延遲、丟包等問(wèn)題一定會(huì)造成網(wǎng)絡(luò)分區(qū),在造成網(wǎng)絡(luò)分區(qū)的情況下,一般有兩個(gè)選擇:CP或者AP。
選擇AP模型實(shí)現(xiàn)分布式鎖時(shí),client在通過(guò)集群主節(jié)點(diǎn)加鎖成功之后,則立刻會(huì)獲取鎖成功的反饋。在主節(jié)點(diǎn)還沒(méi)來(lái)得及把數(shù)據(jù)同步給從節(jié)點(diǎn)時(shí)就發(fā)生宕機(jī)的話(huà),系統(tǒng)會(huì)在從節(jié)點(diǎn)中選出一個(gè)節(jié)點(diǎn)作為新的主節(jié)點(diǎn),新的主節(jié)點(diǎn)沒(méi)有宕機(jī)的主節(jié)點(diǎn)的鎖數(shù)據(jù),導(dǎo)致其他client可以在新的主節(jié)點(diǎn)上拿到相同的鎖。這就會(huì)導(dǎo)致多個(gè)進(jìn)程來(lái)操作相同的臨界資源數(shù)據(jù),從而引發(fā)數(shù)據(jù)不一致性等問(wèn)題。
選擇CP模型實(shí)現(xiàn)分布式鎖,只有在主節(jié)點(diǎn)把數(shù)據(jù)同步給大于1/2的從節(jié)點(diǎn)之后才被視為加鎖成功。此時(shí),主節(jié)點(diǎn)突然宕機(jī),系統(tǒng)會(huì)在從節(jié)點(diǎn)中選取出數(shù)據(jù)比較新的一個(gè)從節(jié)點(diǎn)作為新的主節(jié)點(diǎn),從而避免鎖數(shù)據(jù)丟失的問(wèn)題。
對(duì)于嚴(yán)格的分布式鎖來(lái)說(shuō),CP模型會(huì)更為理想。雖然,基于Redlock實(shí)現(xiàn)的分布式鎖也可以看做是CP模型,但由于需要部署、維護(hù)比較復(fù)雜,在生產(chǎn)環(huán)境很少被使用。所以在對(duì)一致性要求很高的業(yè)務(wù)場(chǎng)景下(電商、銀行支付),一般選擇使用Zookeeper或者etcd。如果可以容忍少量數(shù)據(jù)丟失,出于維護(hù)成本等因素考慮,AP模型的分布式鎖可優(yōu)先選擇Redis。
參考文檔:
https://github.com/redisson/redisson/wiki
https://redis.io/topics/distlock
https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
http://antirez.com/news/101
http://zhangtielei.com/posts/blog-redlock-reasoning.html
http://zhangtielei.com/posts/blog-redlock-reasoning-part2.html
https://github.com/etcd-io/etcd/
