談到分布式系統(tǒng),就不得不談談分布式鎖沟蔑。而主流的實現(xiàn)java分布式鎖實現(xiàn)方案也就那么幾種,有基于Redis實現(xiàn)的,也有基于ZK的,有基于數(shù)據(jù)庫實現(xiàn)的分布式鎖,下面我將談談它們各種的實現(xiàn)方案撞叽。
基于Redis實現(xiàn)分布式鎖
基于Redis實現(xiàn)分布式鎖應該是比較普遍的,實現(xiàn)起來比較簡單.其主要是利用setnx
來實現(xiàn)的,具體語法是setnx key val
,當該key不存在時就設置value,如果已經(jīng)存在該key了就直接返回俏蛮。能這樣做主要得益于Redis的單線程結構,能保證setnx
是原子性的,其偽代碼為:
if (conn.setnx(lockKey, value) == 1) {
}
這樣做存在一個問題:
- 沒有給lockKey設置過期時間,有可能導致該key一直不能釋放,從而使其它線程不能訪問撼泛。
有人立馬反應過來了,"這還不簡單?,用expire
設置一下過期時間即可",所以就有了下面的這段代碼:
if (conn.setnx(lockKey, value) == 1) {
conn.expire(lockKey, expireTime);//expireTime為過期時間
}
到底這樣做有沒有問題呢?考慮一下這種情況,當線程剛剛通過setnx
設置值完畢后,系統(tǒng)因為某個原因宕機(斷點或者系統(tǒng)問題)導致還沒來得及執(zhí)行expire
方法,這時也會引發(fā)和上面同樣的問題。也就是說簡單的設置一個過期時間還不行,因為沒法保證setnx
和expire
是原子操作,隨時都可能setnx
成功但expire
失敗吵冒。幸運的是,Redis提供了這樣一個原子性保證纯命。像Jedis
沒有直接通過setnx
設置過期時間的方法,可以使用這個方法:
@Override
public String set(final String key, final String value, final String nxxx, final String expx,
final long time) {
return new JedisClusterCommand<String>(connectionHandler, maxRedirections) {
@Override
public String execute(Jedis connection) {
return connection.set(key, value, nxxx, expx, time);
}
}.run(key);
}
????????還有一種情況,因為生產(chǎn)環(huán)境上Redis一般都是采用的集群,當master節(jié)點掛了以后會自動切換到slave。當其中一個線程拿到鎖后,此時Redis 的master節(jié)點宕機了,因為Redis是通過異步復制將數(shù)據(jù)同步到從節(jié)點的,鎖信息完全有可能沒有同步成功到從節(jié)點,其它線程就能通過setnx
獲取到鎖,從而引發(fā)多個線程同時獲取鎖的問題痹栖。那該怎么解決呢亿汞?如果業(yè)務上可以接受,我覺得沒必要考慮這種情況,因為這種情況出現(xiàn)的幾率非常少,反之就必須想一個解決方案。
如果Redis是單主單從的話,這個問題基于Redis很難解決,如果是多主多從,可以考慮對所有的master都加鎖,即使其中一個master獲取鎖失敗,也不會影響其它兩個節(jié)點,當總數(shù)超過一半時也讓其獲取到鎖结耀。因為涉及到跨多個節(jié)點,需要小心的控制超時時間和鎖的釋放問題留夜。
最后,在方法結束時和拋出異常的時候需要手動釋放鎖,最好是在finally執(zhí)行。
基于Zookeeper實現(xiàn)分布式鎖
一般用Zookeeper實現(xiàn)分布式鎖有兩種方案:
- 基于Zookeeper不能重復創(chuàng)建同一個節(jié)點
利用名稱唯一性图甜,加鎖操作時碍粥,只需要所有客戶端一起創(chuàng)建/Lock/test節(jié)點,只有一個創(chuàng)建成功黑毅,成功者獲得鎖嚼摩。解鎖時,只需刪除/Lock/test節(jié)點,其余客戶端再次進入競爭創(chuàng)建節(jié)點枕面,直到所有客戶端都獲得鎖.
這種方案的正確性和可靠性是ZooKeeper機制保證的愿卒,實現(xiàn)簡單。缺點是會產(chǎn)生“驚群”效應潮秘,假如許多客戶端在等待一把鎖琼开,當鎖釋放時候所有客戶端都被喚醒,僅僅有一個客戶端得到鎖枕荞。 - 基于臨時有序節(jié)點
對于加鎖操作柜候,可以讓所有客戶端都去/Lock目錄下創(chuàng)建臨時順序節(jié)點,如果創(chuàng)建的客戶端發(fā)現(xiàn)自身創(chuàng)建節(jié)點序列號是/Lock/目錄下最小的節(jié)點躏精,則獲得鎖渣刷。否則,監(jiān)視比自己創(chuàng)建節(jié)點的序列號小的節(jié)點(比自己創(chuàng)建的節(jié)點小的最大節(jié)點).進入等待矗烛。對于解鎖操作辅柴,只需要將自身創(chuàng)建的節(jié)點刪除即可,然后喚醒自己的后一個節(jié)點。
特點:利用臨時順序節(jié)點來實現(xiàn)分布式鎖機制其實就是一種按照創(chuàng)建順序排隊的實現(xiàn)瞭吃。這種方案效率高碌嘀,避免了“驚群”效應,多個客戶端共同等待鎖虱而,當鎖釋放時只有一個客戶端會被喚醒筏餐。
由于Curator客戶端已經(jīng)提供了分布式鎖的實現(xiàn),可以直接使用如下代碼:
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) ) {
try
{
} finally {
lock.release();
}
}
基于數(shù)據(jù)庫實現(xiàn)分布式鎖
利用DB來實現(xiàn)分布式鎖,有兩種方案牡拇。兩種方案各有好壞,但是總體效果都不是很好穆律。但是實現(xiàn)還是比較簡單的惠呼。
- 利用主鍵唯一約束:
我們知道數(shù)據(jù)庫是有唯一主鍵規(guī)則的,主鍵不能重復峦耘,對于重復的主鍵會拋出主鍵沖突異常剔蹋。
其實這和分布式鎖實現(xiàn)方案基本是一致的,首先我們利用主鍵唯一規(guī)則辅髓,在爭搶鎖的時候向DB中寫一條記錄泣崩,這條記錄主要包含鎖的id、當前占用鎖的線程名洛口、重入的次數(shù)和創(chuàng)建時間等矫付,如果插入成功表示當前線程獲取到了鎖,如果插入失敗那么證明鎖被其他人占用第焰,等待一會兒繼續(xù)爭搶买优,直到爭搶到或者超時為止 - 利用Mysql行鎖的特性:
利用for update加顯式的行鎖,這樣就能利用這個行級的排他鎖來實現(xiàn)分布式鎖了,同時unlock的時候只要釋放commit這個事務杀赢,就能達到釋放鎖的目的烘跺。
總結
Redis做分布式鎖實現(xiàn)起來比較簡單,如果不考慮master宕機引發(fā)的并發(fā)獲取鎖的問題,通過簡單的setnx
就可以實現(xiàn),性能也很好。
如果項目中已經(jīng)使用了ZK也可以考慮使用使用zk來做分布式鎖,使用curator封裝好的分布式鎖即可,沒必要自己實現(xiàn)脂崔。但是zk的主要優(yōu)勢還是做分布式協(xié)調(diào),如果針對大壓力場景下可能會引發(fā)性能問題,最好將其它業(yè)務進行隔離滤淳。
數(shù)據(jù)庫做分布式鎖適合壓力比較小的情況,因為頻繁的for update可能造成數(shù)據(jù)庫連接的的耗盡,從而引發(fā)單點問題。