基于數(shù)據(jù)庫、redis和zookeeper實現(xiàn)的分布式鎖
點擊上方藍色字體,選擇“標星公眾號”
優(yōu)質(zhì)文章,第一時間送達
? 作者?|??曹自標
來源 |? urlify.cn/aMJJrm
基于數(shù)據(jù)庫
基于數(shù)據(jù)庫(MySQL)的方案,一般分為3類:基于表記錄、樂觀鎖和悲觀鎖
基于表記錄
用表主鍵或表字段加唯一性索引便可實現(xiàn),如下;
CREATE?TABLE?`database_lock`?(
?`id`?BIGINT?NOT?NULL?AUTO_INCREMENT,
?`resource`?int?NOT?NULL?COMMENT?'鎖定的資源',
?`description`?varchar(1024)?NOT?NULL?DEFAULT?""?COMMENT?'描述',
?PRIMARY?KEY?(`id`),
?UNIQUE?KEY?`uiq_idx_resource`?(`resource`)?
)?ENGINE=InnoDB?DEFAULT?CHARSET=utf8mb4?COMMENT='數(shù)據(jù)庫分布式鎖表';
想獲得鎖插入一條數(shù)據(jù)
INSERT?INTO?database_lock(resource,?description)?VALUES?(1,?'lock');
解鎖刪除數(shù)據(jù):
DELETE?FROM?database_lock?WHERE?resource=1;
這種實現(xiàn)方式非常的簡單,但是需要注意以下幾點:
這種鎖沒有失效時間,一旦釋放鎖的操作失敗就會導致鎖記錄一直在數(shù)據(jù)庫中,其它線程無法獲得鎖。這個缺陷也很好解決,比如可以做一個定時任務去定時清理。
這種鎖的可靠性依賴于數(shù)據(jù)庫。建議設置備庫,避免單點,進一步提高可靠性。
這種鎖是非阻塞的,因為插入數(shù)據(jù)失敗之后會直接報錯,想要獲得鎖就需要再次操作。如果需要阻塞式的,可以弄個for循環(huán)、while循環(huán)之類的,直至INSERT成功再返回。
這種鎖也是非可重入的,因為同一個線程在沒有釋放鎖之前無法再次獲得鎖,因為數(shù)據(jù)庫中已經(jīng)存在同一份記錄了。想要實現(xiàn)可重入鎖,可以在數(shù)據(jù)庫中添加一些字段,比如獲得鎖的主機信息、線程信息等,那么在再次獲得鎖的時候可以先查詢數(shù)據(jù),如果當前的主機信息和線程信息等能被查到的話,可以直接把鎖分配給它。
在 MySQL 數(shù)據(jù)庫中采用主鍵沖突防重,在大并發(fā)情況下有可能會造成鎖表現(xiàn)象
基于樂觀鎖
可基于MVCC機制實現(xiàn)
優(yōu)點:在檢測數(shù)據(jù)沖突時并不依賴數(shù)據(jù)庫本身的鎖機制,不會影響請求的性能,當產(chǎn)生并發(fā)且并發(fā)量較小的時候只有少部分請求會失敗
缺點:唯一癿問題就是對數(shù)據(jù)表侵入較大,我們
要為每個表設計一個版本號字段,然后寫一條判斷 sql 每次進行判斷,增加了數(shù)據(jù)庫操作的次數(shù),在高并發(fā)要求下,對數(shù)據(jù)庫連接的開銷也是無法忍受的。
基于悲觀鎖
在查詢語句后面增加for update, 數(shù)據(jù)庫會在查詢過程中給數(shù)據(jù)庫表增加排他鎖, 當某條記錄被加上排他鎖之后,其他線程無法再在該行記錄上增加排他鎖。
我們可以任務獲得排他鎖的線程即可獲得分布式鎖,當獲取到鎖之后,可以執(zhí)行方法的業(yè)務邏輯,執(zhí)行完方法后,通過connection.commit()操作來釋放鎖
注意:在加鎖的時候,只有明確地指定主鍵(或索引)的才會執(zhí)行行鎖,否則MySQL 將會執(zhí)行表鎖
加鎖前注意取消自動提交
優(yōu)點:
簡單易于理解
嚴格保證數(shù)據(jù)訪問的安全
缺點:
MySQL會對查詢進行優(yōu)化,如果任務全表掃描效率更高,便使用表鎖,導致性能問題
如果一個排他鎖長時間不提交,就會占用數(shù)據(jù)庫連接,類似連接變多,就可能把連接池撐爆
悲觀鎖使用不當還可能產(chǎn)生死鎖的情況
每次請求都會額外產(chǎn)生加鎖的開銷且未獲取到鎖的請求將會阻塞等待鎖的獲取,在高并發(fā)環(huán)境下,容易造成大量請求阻塞,影響系統(tǒng)可用性
基于redis
Java jedis分布式鎖例子
依賴(注意版本2.9.0后,但3以上不支持)
????redis.clients
????jedis
????2.9.0
public?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";
????/**
?????*?嘗試獲取分布式鎖
?????*?@param?jedis?Redis客戶端
?????*?@param?lockKey?鎖
?????*?@param?requestId?請求標識
?????*?@param?expireTime?超期時間
?????*?@return?是否獲取成功
?????*/
????public?static?boolean?tryGetDistributedLock(Jedis?jedis,?String?lockKey,?String?requestId,?int?expireTime)?{
????????/**
?????????*?1.?使用key來當鎖,因為key是唯一的
?????????* 2. value,傳的是requestId。通過給value賦值為requestId,我們就知道這把鎖是哪個請求加的了,在解鎖的時候就可以有依據(jù)
?????????*?3.?NX,意思是SET?IF?NOT?EXIST,即當key不存在時,我們進行set操作;若key已經(jīng)存在,則不做任何操作;
?????????* 4. PX,意思是我們要給這個key加一個過期的設置,具體時間由第五個參數(shù)決定。
?????????*?5.?time,代表key的過期時間
?????????*/
????????String?result?=?jedis.set(lockKey,?requestId,?SET_IF_NOT_EXIST,?SET_WITH_EXPIRE_TIME,?expireTime);
????????if?(LOCK_SUCCESS.equals(result))?{
????????????return?true;
????????}
????????return?false;
????}
????
????private?static?final?Long?RELEASE_SUCCESS?=?1L;
????/**
?????*?釋放分布式鎖
?????*?@param?jedis?Redis客戶端
?????*?@param?lockKey?鎖
?????*?@param?requestId?請求標識
?????*?@return?是否釋放成功
?????*/
????public?static?boolean?releaseDistributedLock(Jedis?jedis,?String?lockKey,?String?requestId)?{
????????/**
?????????*?使用Lua語言來實現(xiàn),來確保上述操作是原子性。在eval命令執(zhí)行Lua代碼的時候,Lua代碼將被當成一個命令去執(zhí)行,并且直到eval命令執(zhí)行完成,Redis才會執(zhí)行其他命令。
?????????*?參數(shù)KEYS[1]賦值為lockKey,ARGV[1]賦值為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;
????}
}
執(zhí)行上面的set()方法就只會導致兩種結果:
當前沒有鎖(key不存在),那么就進行加鎖操作,并對鎖設置個有效期,同時value表示加鎖的客戶端。
已有鎖存在,不做任何操作。
Redisson實現(xiàn)分布式鎖
使用流程如下,創(chuàng)建Redisson實例(單機或哨兵模式),然后通過getLock獲取鎖,后續(xù)是進行l(wèi)ock和unlock操作。
//?1.?Create?config?object
Config?config?=?new?Config();
config.useClusterServers()
???????//?use?"rediss://"?for?SSL?connection
??????.addNodeAddress("redis://127.0.0.1:7181");
//?2.?Create?Redisson?instance
//?Sync?and?Async?API
RedissonClient?redisson?=?Redisson.create(config);
//?3.?Get?Redis?based?implementation?of?java.util.concurrent.locks.Lock
RLock?lock?=?redisson.getLock("myLock");
具體使用例子可參考:https://www.cnblogs.com/milicool/p/9201271.html
基于zookeeper
zookeeper基本鎖原理
利用臨時節(jié)點與watch機制,每個鎖占用一個普通節(jié)點/lock,當需要獲取鎖時,在/lock目錄下創(chuàng)建一個臨時節(jié)點,創(chuàng)建成功則表示獲取鎖成功,失敗則watch /lock節(jié)點,有刪除操作后再去爭鎖。
臨時節(jié)點
好處:在于當進程掛掉后能自動上鎖的節(jié)點自動刪除,即取消鎖
缺點:所有取鎖失敗的進程都監(jiān)聽父節(jié)點,很容易發(fā)生羊群效應,即當釋放鎖后所有等待進程一起來創(chuàng)建節(jié)點,并發(fā)量很大
zookeeper鎖優(yōu)化原理
上鎖改為創(chuàng)建臨時有序節(jié)點,每個上鎖的節(jié)點均能創(chuàng)建節(jié)點成功,只是其序號不同,只有序號最小的可以擁有鎖,如果這個節(jié)點序號不是最小的則watch序號比本身小的前一個節(jié)點。
步驟:
在/lock節(jié)點下創(chuàng)建一個有序臨時節(jié)點(EPHEMERAL_SEQUENTIAL)
判斷創(chuàng)建的節(jié)點序號是否最小,如果是則獲取鎖成功。不是則獲取鎖失敗,watch序號比本身小的前一個節(jié)點(避免很多線程watch同一個node,導致羊群效應)
當獲取鎖失敗,設置watch后則等待watch事件到來后,再次判斷是否序號最小
取鎖成功則執(zhí)行代碼,最后釋放鎖(刪除該節(jié)點)
優(yōu)缺點:
優(yōu)點:有效的解決單點問題,不可重入問題,非阻塞問題,以及鎖無法釋放問題。實現(xiàn)簡單
缺點:性能上可能沒有緩存服務高,因為每次在創(chuàng)建鎖和釋放鎖過程中,都要動態(tài)創(chuàng)建、銷毀臨時節(jié)點來實現(xiàn)鎖功能。zookeeper中創(chuàng)建和刪除節(jié)點只能通過Leader服務器來執(zhí)行,然后將數(shù)據(jù)同步到所有follower機器上。

(圖片來自https://mp.weixin.qq.com/s/jn4LkPKlWJhfUwIKkp3KpQ)
開源框架Curator
Curator開源框架對zookeeper分布式鎖進行了實現(xiàn)。具體例子可參考:https://www.jianshu.com/p/31335efec309
粉絲福利:Java從入門到入土學習路線圖
???

?長按上方微信二維碼?2 秒
感謝點贊支持下哈?
