阿里JAVA面試題剖析:一般實現(xiàn)分布式鎖都有哪些方式淡喜?使用 Redis 如何設(shè)計分布式鎖炼团?使用 zk 來設(shè)計分布式鎖可以嗎疏尿?這兩種分布式鎖的實現(xiàn)方式哪種效率比較高?

面試原題

一般實現(xiàn)分布式鎖都有哪些方式锌俱?使用 redis 如何設(shè)計分布式鎖贸宏?使用 zk 來設(shè)計分布式鎖可以嗎?這兩種分布式鎖的實現(xiàn)方式哪種效率比較高吭练?

面試官心理分析

其實一般問問題鲫咽,都是這么問的,先問問你 zk姊舵,然后其實是要過度到 zk 關(guān)聯(lián)的一些問題里去括丁,比如分布式鎖伶选。因為在分布式系統(tǒng)開發(fā)中仰税,分布式鎖的使用場景還是很常見的。

面試題剖析

redis 分布式鎖

官方叫做?RedLock?算法吐绵,是 redis 官方支持的分布式鎖算法己单。

這個分布式鎖有 3 個重要的考量點:

互斥(只能有一個客戶端獲取鎖)

不能死鎖

容錯(只要大部分 redis 節(jié)點創(chuàng)建了這把鎖就可以)

redis 最普通的分布式鎖

第一個最普通的實現(xiàn)方式纹笼,就是在 redis 里創(chuàng)建一個 key苟跪,這樣就算加鎖件已。

SETmy:lock隨機值NXPX30000

執(zhí)行這個命令就 ok。

NX:表示只有?key?不存在的時候才會設(shè)置成功鳞陨。(如果此時 redis 中存在這個 key厦滤,那么設(shè)置失敗,返回?nil)

PX 30000:意思是 30s 后鎖自動釋放享怀。別人創(chuàng)建的時候如果發(fā)現(xiàn)已經(jīng)有了就不能加鎖了添瓷。

釋放鎖就是刪除 key 值纱,但是一般可以用?lua?腳本刪除,判斷 value 一樣才刪除:

-- 刪除鎖的時候搀愧,找到 key 對應(yīng)的 value咱筛,跟自己傳過去的 value 做比較杆故,如果是一樣的才刪除处铛。

if redis.call("get",KEYS[1]) == ARGV[1] then

? ? ? ? ? return redis.call("del",KEYS[1])

else

? ? ? ? ? ?return 0

end

為啥要用隨機值呢?因為如果某個客戶端獲取到了鎖篙贸,但是阻塞了很長時間才執(zhí)行完,比如說超過了 30s敷鸦,此時可能已經(jīng)自動釋放鎖了扒披,此時可能別的客戶端已經(jīng)獲取到了這個鎖,要是你這個時候直接刪除 key 的話會有問題愿险,所以得用隨機值加上面的?lua?腳本來釋放鎖辆亏。

但是這樣是肯定不行的。因為如果是普通的 redis 單實例缤弦,那就是單點故障彻磁≈则眩或者是 redis 普通主從,那 redis 主從異步復(fù)制刻恭,如果主節(jié)點掛了(key 就沒有了)鳍贾,key 還沒同步到從節(jié)點交洗,此時從節(jié)點切換為主節(jié)點,別人就可以 set key咆爽,從而拿到鎖斗埂。

RedLock 算法

這個場景是假設(shè)有一個 redis cluster凫海,有 5 個 redis master 實例行贪。然后執(zhí)行如下步驟獲取一把鎖:

獲取當前時間戳,單位是毫秒崭捍;

跟上面類似殷蛇,輪流嘗試在每個 master 節(jié)點上創(chuàng)建鎖,過期時間較短亮航,一般就幾十毫秒塞赂;

嘗試在大多數(shù)節(jié)點上建立一個鎖昼蛀,比如 5 個節(jié)點就要求是 3 個節(jié)點?n / 2 + 1叼旋;

客戶端計算建立好鎖的時間,如果建立鎖的時間小于超時時間讹剔,就算建立成功了延欠;

要是鎖建立失敗了沈跨,那么就依次之前建立過的鎖刪除;

只要別人建立了一把分布式鎖狞玛,你就得不斷輪詢?nèi)L試獲取鎖涧窒。

zk 分布式鎖

zk 分布式鎖纠吴,其實可以做的比較簡單呜象,就是某個節(jié)點嘗試創(chuàng)建臨時 znode,此時創(chuàng)建成功了就獲取了這個鎖;這個時候別的客戶端來創(chuàng)建鎖會失敗上煤,只能注冊個監(jiān)聽器監(jiān)聽這個鎖。釋放鎖就是刪除這個 znode拴疤,一旦釋放掉就會通知客戶端,然后有一個等待著的客戶端就可以再次重新加鎖苔埋。

/**

* ZooKeeperSession

*

* @author bingo

* @since 2018/11/29

*

*/

public class ZooKeeperSession {

? ? private static CountDownLatch connectedSemaphore = new CountDownLatch(1);

? ? private ZooKeeper zookeeper;

? ? private CountDownLatch latch;

? ? public ZooKeeperSession() {

? ? ? ? try {

? ? ? ? ? ? this.zookeeper = new ZooKeeper("192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 50000, new ZooKeeperWatcher());

? ? ? ? ? ? try {

? ? ? ? ? ? ? ? connectedSemaphore.await();

? ? ? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? ? ? e.printStackTrace();

? ? ? ? ? ? }

? ? ? ? ? ? System.out.println("ZooKeeper session established......");

? ? ? ? } catch (Exception e) {

? ? ? ? ? ? e.printStackTrace();

? ? ? ? }

? ? }

? ? /**

? ?? * 獲取分布式鎖

? ?? *

? ?? * @param productId

? ?? */

? ? public Boolean acquireDistributedLock(Long productId) {

? ? ? ? String path = "/product-lock-" + productId;

? ? ? ? try {

? ? ? ? ? ? zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);

? ? ? ? ? ? return true;

? ? ? ? } catch (Exception e) {

? ? ? ? ? ? while (true) {

? ? ? ? ? ? ? ? try {

? ? ? ? ? ? ? ? ? ? // 相當于是給node注冊一個監(jiān)聽器,去看看這個監(jiān)聽器是否存在

? ? ? ? ? ? ? ? ? ? Stat stat = zk.exists(path, true);

? ? ? ? ? ? ? ? ? ? if (stat != null) {

? ? ? ? ? ? ? ? ? ? ? ? this.latch = new CountDownLatch(1);

? ? ? ? ? ? ? ? ? ? ? ? this.latch.await(waitTime, TimeUnit.MILLISECONDS);

? ? ? ? ? ? ? ? ? ? ? ? this.latch = null;

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);

? ? ? ? ? ? ? ? ? ? return true;

? ? ? ? ? ? ? ? } catch (Exception ee) {

? ? ? ? ? ? ? ? ? ? continue;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? return true;

? ? }

? ? /**

? ?? * 釋放掉一個分布式鎖

? ?? *

? ?? * @param productId

? ?? */

? ? public void releaseDistributedLock(Long productId) {

? ? ? ? String path = "/product-lock-" + productId;

? ? ? ? try {

? ? ? ? ? ? zookeeper.delete(path, -1);

? ? ? ? ? ? System.out.println("release the lock for product[id=" + productId + "]......");

? ? ? ? } catch (Exception e) {

? ? ? ? ? ? e.printStackTrace();

? ? ? ? }

? ? }

? ? /**

? ?? * 建立zk session的watcher

? ?? *

? ?? * @author bingo

? ?? * @since 2018/11/29

? ?? *

? ?? */

? ? private class ZooKeeperWatcher implements Watcher {

? ? ? ? public void process(WatchedEvent event) {

? ? ? ? ? ? System.out.println("Receive watched event: " + event.getState());

? ? ? ? ? ? if (KeeperState.SyncConnected == event.getState()) {

? ? ? ? ? ? ? ? connectedSemaphore.countDown();

? ? ? ? ? ? }

? ? ? ? ? ? if (this.latch != null) {

? ? ? ? ? ? ? ? this.latch.countDown();

? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? /**

? ?? * 封裝單例的靜態(tài)內(nèi)部類

? ?? *

? ?? * @author bingo

? ?? * @since 2018/11/29

? ?? *

? ?? */

? ? private static class Singleton {

? ? ? ? private static ZooKeeperSession instance;

? ? ? ? static {

? ? ? ? ? ? instance = new ZooKeeperSession();

? ? ? ? }

? ? ? ? public static ZooKeeperSession getInstance() {

? ? ? ? ? ? return instance;

? ? ? ? }

? ? }

? ? /**

? ?? * 獲取單例

? ?? *

? ?? * @return

? ?? */

? ? public static ZooKeeperSession getInstance() {

? ? ? ? return Singleton.getInstance();

? ? }

? ? /**

? ?? * 初始化單例的便捷方法

? ?? */

? ? public static void init() {

? ? ? ? getInstance();

? ? }

}

也可以采用另一種方式玉工,創(chuàng)建臨時順序節(jié)點:

如果有一把鎖遵班,被多個人給競爭狭郑,此時多個人會排隊汇在,第一個拿到鎖的人會執(zhí)行,然后釋放鎖缨历;后面的每個人都會去監(jiān)聽排在自己前面的那個人創(chuàng)建的 node 上辛孵,一旦某個人釋放了鎖赡磅,排在自己后面的人就會被 zookeeper 給通知,一旦被通知了之后冶匹,就 ok 了咆瘟,自己就獲取到了鎖袒餐,就可以執(zhí)行代碼了谤狡。

public class ZooKeeperDistributedLock implements Watcher {

? ? private ZooKeeper zk;

? ? private String locksRoot = "/locks";

? ? private String productId;

? ? private String waitNode;

? ? private String lockNode;

? ? private CountDownLatch latch;

? ? private CountDownLatch connectedLatch = new CountDownLatch(1);

? ? private int sessionTimeout = 30000;

? ? public ZooKeeperDistributedLock(String productId) {

? ? ? ? this.productId = productId;

? ? ? ? try {

? ? ? ? ? ? String address = "192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181";

? ? ? ? ? ? zk = new ZooKeeper(address, sessionTimeout, this);

? ? ? ? ? ? connectedLatch.await();

? ? ? ? } catch (IOException e) {

? ? ? ? ? ? throw new LockException(e);

? ? ? ? } catch (KeeperException e) {

? ? ? ? ? ? throw new LockException(e);

? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? throw new LockException(e);

? ? ? ? }

? ? }

? ? public void process(WatchedEvent event) {

? ? ? ? if (event.getState() == KeeperState.SyncConnected) {

? ? ? ? ? ? connectedLatch.countDown();

? ? ? ? ? ? return;

? ? ? ? }

? ? ? ? if (this.latch != null) {

? ? ? ? ? ? this.latch.countDown();

? ? ? ? }

? ? }

? ? public void acquireDistributedLock() {

? ? ? ? try {

? ? ? ? ? ? if (this.tryLock()) {

? ? ? ? ? ? ? ? return;

? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? waitForLock(waitNode, sessionTimeout);

? ? ? ? ? ? }

? ? ? ? } catch (KeeperException e) {

? ? ? ? ? ? throw new LockException(e);

? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? throw new LockException(e);

? ? ? ? }

? ? }

? ? public boolean tryLock() {

? ? ? ? try {

? ? // 傳入進去的locksRoot + “/” + productId

? ? // 假設(shè)productId代表了一個商品id,比如說1

? ? // locksRoot = locks

? ? // /locks/10000000000捕仔,/locks/10000000001盈罐,/locks/10000000002

? ? ? ? ? ? lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

? ? ? ? ? ? // 看看剛創(chuàng)建的節(jié)點是不是最小的節(jié)點

? ? // locks:10000000000暖呕,10000000001,10000000002

? ? ? ? ? ? List<String> locks = zk.getChildren(locksRoot, false);

? ? ? ? ? ? Collections.sort(locks);

? ? ? ? ? ? if(lockNode.equals(locksRoot+"/"+ locks.get(0))){

? ? ? ? ? ? ? ? //如果是最小的節(jié)點,則表示取得鎖

? ? ? ? ? ? ? ? return true;

? ? ? ? ? ? }

? ? ? ? ? ? //如果不是最小的節(jié)點瓤逼,找到比自己小1的節(jié)點

? int previousLockIndex = -1;

? ? ? ? ? ? for(int i = 0; i < locks.size(); i++) {

if(lockNode.equals(locksRoot + “/” + locks.get(i))) {

? ? ? ?? ? ? previousLockIndex = i - 1;

? ? break;

}

?? }

?? this.waitNode = locks.get(previousLockIndex);

? ? ? ? } catch (KeeperException e) {

? ? ? ? ? ? throw new LockException(e);

? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? throw new LockException(e);

? ? ? ? }

? ? ? ? return false;

? ? }

? ? private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {

? ? ? ? Stat stat = zk.exists(locksRoot + "/" + waitNode, true);

? ? ? ? if (stat != null) {

? ? ? ? ? ? this.latch = new CountDownLatch(1);

? ? ? ? ? ? this.latch.await(waitTime, TimeUnit.MILLISECONDS);

? ? ? ? ? ? this.latch = null;

? ? ? ? }

? ? ? ? return true;

? ? }

? ? public void unlock() {

? ? ? ? try {

? ? ? ? ? ? // 刪除/locks/10000000000節(jié)點

? ? ? ? ? ? // 刪除/locks/10000000001節(jié)點

? ? ? ? ? ? System.out.println("unlock " + lockNode);

? ? ? ? ? ? zk.delete(lockNode, -1);

? ? ? ? ? ? lockNode = null;

? ? ? ? ? ? zk.close();

? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? e.printStackTrace();

? ? ? ? } catch (KeeperException e) {

? ? ? ? ? ? e.printStackTrace();

? ? ? ? }

? ? }

? ? public class LockException extends RuntimeException {

? ? ? ? private static final long serialVersionUID = 1L;

? ? ? ? public LockException(String e) {

? ? ? ? ? ? super(e);

? ? ? ? }

? ? ? ? public LockException(Exception e) {

? ? ? ? ? ? super(e);

? ? ? ? }

? ? }

}

redis 分布式鎖和 zk 分布式鎖的對比

redis 分布式鎖霸旗,其實需要自己不斷去嘗試獲取鎖诱告,比較消耗性能民晒。

zk 分布式鎖,獲取不到鎖靴姿,注冊個監(jiān)聽器即可,不需要不斷主動嘗試獲取鎖佛吓,性能開銷較小维雇。

另外一點就是晒他,如果是 redis 獲取鎖的那個客戶端 出現(xiàn) bug 掛了,那么只能等待超時時間之后才能釋放鎖唁影;而 zk 的話掂名,因為創(chuàng)建的是臨時 znode,只要客戶端掛了锌介,znode 就沒了孔祸,此時就自動釋放鎖发皿。

redis 分布式鎖大家沒發(fā)現(xiàn)好麻煩嗎?遍歷上鎖惶室,計算時間等等......zk 的分布式鎖語義清晰實現(xiàn)簡單玄货。

所以先不分析太多的東西松捉,就說這兩點,我個人實踐認為 zk 的分布式鎖比 redis 的分布式鎖牢靠倚评、而且模型簡單易用。

需要更多java架構(gòu)資料及面試真題請關(guān)注私信回復(fù) 666?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市永票,隨后出現(xiàn)的幾起案子侣集,更是在濱河造成了極大的恐慌兰绣,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件踪央,死亡現(xiàn)場離奇詭異,居然都是意外死亡畅蹂,警方通過查閱死者的電腦和手機液斜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門叠穆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來硼被,“玉大人,你說我怎么就攤上這事检访÷畚。” “怎么了嘉汰?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵双泪,是天一觀的道長。 經(jīng)常有香客問我焙矛,道長村斟,這世上最難降的妖魔是什么抛猫? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任逾滥,我火速辦了婚禮败匹,結(jié)果婚禮上讥巡,老公的妹妹穿的比我還像新娘欢顷。我一直安慰自己吱涉,他們只是感情好外里,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著特石,像睡著了一般盅蝗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上姆蘸,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天墩莫,我揣著相機與錄音,去河邊找鬼逞敷。 笑死狂秦,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的推捐。 我是一名探鬼主播裂问,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼牛柒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起椭更,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤滴须,失蹤者是張志新(化名)和其女友劉穎把夸,沒想到半個月后膀篮,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了扼倘。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡泛豪,死狀恐怖吕粹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情稳其,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站龄恋,受9級特大地震影響郭毕,放射性物質(zhì)發(fā)生泄漏傻挂。R本人自食惡果不足惜踊谋,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一睦疫、第九天 我趴在偏房一處隱蔽的房頂上張望蛤育。 院中可真熱鬧腋么,春花似錦圣勒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春槽华,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人基显。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓摸航,卻偏偏與公主長得像酱虎,于是被迫代替她去往敵國和親聊记。 傳聞我的和親對象是個殘疾皇子舆床,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353