面試原題
一般實(shí)現(xiàn)分布式鎖都有哪些方式?使用 redis 如何設(shè)計(jì)分布式鎖?使用 zk 來設(shè)計(jì)分布式鎖可以嗎?這兩種分布式鎖的實(shí)現(xiàn)方式哪種效率比較高?
面試官心理分析
其實(shí)一般問問題,都是這么問的,先問問你 zk,然后其實(shí)是要過度到 zk 關(guān)聯(lián)的一些問題里去,比如分布式鎖。因?yàn)樵诜植际较到y(tǒng)開發(fā)中,分布式鎖的使用場景還是很常見的。

面試題剖析
redis 分布式鎖
官方叫做?RedLock?算法,是 redis 官方支持的分布式鎖算法。
這個(gè)分布式鎖有 3 個(gè)重要的考量點(diǎn):
互斥(只能有一個(gè)客戶端獲取鎖)
不能死鎖
容錯(cuò)(只要大部分 redis 節(jié)點(diǎn)創(chuàng)建了這把鎖就可以)
redis 最普通的分布式鎖
第一個(gè)最普通的實(shí)現(xiàn)方式,就是在 redis 里創(chuàng)建一個(gè) key,這樣就算加鎖。
SETmy:lock隨機(jī)值NXPX30000
執(zhí)行這個(gè)命令就 ok。
NX:表示只有?key?不存在的時(shí)候才會設(shè)置成功。(如果此時(shí) redis 中存在這個(gè) key,那么設(shè)置失敗,返回?nil)
PX 30000:意思是 30s 后鎖自動(dòng)釋放。別人創(chuàng)建的時(shí)候如果發(fā)現(xiàn)已經(jīng)有了就不能加鎖了。
釋放鎖就是刪除 key ,但是一般可以用?lua?腳本刪除,判斷 value 一樣才刪除:
-- 刪除鎖的時(shí)候,找到 key 對應(yīng)的 value,跟自己傳過去的 value 做比較,如果是一樣的才刪除。
if redis.call("get",KEYS[1]) == ARGV[1] then
? ? ? ? ? return redis.call("del",KEYS[1])
else
? ? ? ? ? ?return 0
end
為啥要用隨機(jī)值呢?因?yàn)槿绻硞€(gè)客戶端獲取到了鎖,但是阻塞了很長時(shí)間才執(zhí)行完,比如說超過了 30s,此時(shí)可能已經(jīng)自動(dòng)釋放鎖了,此時(shí)可能別的客戶端已經(jīng)獲取到了這個(gè)鎖,要是你這個(gè)時(shí)候直接刪除 key 的話會有問題,所以得用隨機(jī)值加上面的?lua?腳本來釋放鎖。
但是這樣是肯定不行的。因?yàn)槿绻瞧胀ǖ?redis 單實(shí)例,那就是單點(diǎn)故障?;蛘呤?redis 普通主從,那 redis 主從異步復(fù)制,如果主節(jié)點(diǎn)掛了(key 就沒有了),key 還沒同步到從節(jié)點(diǎn),此時(shí)從節(jié)點(diǎn)切換為主節(jié)點(diǎn),別人就可以 set key,從而拿到鎖。
RedLock 算法
這個(gè)場景是假設(shè)有一個(gè) redis cluster,有 5 個(gè) redis master 實(shí)例。然后執(zhí)行如下步驟獲取一把鎖:
獲取當(dāng)前時(shí)間戳,單位是毫秒;
跟上面類似,輪流嘗試在每個(gè) master 節(jié)點(diǎn)上創(chuàng)建鎖,過期時(shí)間較短,一般就幾十毫秒;
嘗試在大多數(shù)節(jié)點(diǎn)上建立一個(gè)鎖,比如 5 個(gè)節(jié)點(diǎn)就要求是 3 個(gè)節(jié)點(diǎn)?n / 2 + 1;
客戶端計(jì)算建立好鎖的時(shí)間,如果建立鎖的時(shí)間小于超時(shí)時(shí)間,就算建立成功了;
要是鎖建立失敗了,那么就依次之前建立過的鎖刪除;
只要?jiǎng)e人建立了一把分布式鎖,你就得不斷輪詢?nèi)L試獲取鎖。

zk 分布式鎖
zk 分布式鎖,其實(shí)可以做的比較簡單,就是某個(gè)節(jié)點(diǎn)嘗試創(chuàng)建臨時(shí) znode,此時(shí)創(chuàng)建成功了就獲取了這個(gè)鎖;這個(gè)時(shí)候別的客戶端來創(chuàng)建鎖會失敗,只能注冊個(gè)監(jiān)聽器監(jiān)聽這個(gè)鎖。釋放鎖就是刪除這個(gè) znode,一旦釋放掉就會通知客戶端,然后有一個(gè)等待著的客戶端就可以再次重新加鎖。
/**
* 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 {
? ? ? ? ? ? ? ? ? ? // 相當(dāng)于是給node注冊一個(gè)監(jiān)聽器,去看看這個(gè)監(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;
? ? }
? ? /**
? ?? * 釋放掉一個(gè)分布式鎖
? ?? *
? ?? * @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)建臨時(shí)順序節(jié)點(diǎn):
如果有一把鎖,被多個(gè)人給競爭,此時(shí)多個(gè)人會排隊(duì),第一個(gè)拿到鎖的人會執(zhí)行,然后釋放鎖;后面的每個(gè)人都會去監(jiān)聽排在自己前面的那個(gè)人創(chuàng)建的 node 上,一旦某個(gè)人釋放了鎖,排在自己后面的人就會被 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 {
? ? // 傳入進(jìn)去的locksRoot + “/” + productId
? ? // 假設(shè)productId代表了一個(gè)商品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é)點(diǎn)是不是最小的節(jié)點(diǎn)
? ? // locks:10000000000,10000000001,10000000002
? ? ? ? ? ? List<String> locks = zk.getChildren(locksRoot, false);
? ? ? ? ? ? Collections.sort(locks);
? ? ? ? ? ? if(lockNode.equals(locksRoot+"/"+ locks.get(0))){
? ? ? ? ? ? ? ? //如果是最小的節(jié)點(diǎn),則表示取得鎖
? ? ? ? ? ? ? ? return true;
? ? ? ? ? ? }
? ? ? ? ? ? //如果不是最小的節(jié)點(diǎn),找到比自己小1的節(jié)點(diǎn)
? 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é)點(diǎn)
? ? ? ? ? ? // 刪除/locks/10000000001節(jié)點(diǎn)
? ? ? ? ? ? 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 分布式鎖,其實(shí)需要自己不斷去嘗試獲取鎖,比較消耗性能。
zk 分布式鎖,獲取不到鎖,注冊個(gè)監(jiān)聽器即可,不需要不斷主動(dòng)嘗試獲取鎖,性能開銷較小。
另外一點(diǎn)就是,如果是 redis 獲取鎖的那個(gè)客戶端 出現(xiàn) bug 掛了,那么只能等待超時(shí)時(shí)間之后才能釋放鎖;而 zk 的話,因?yàn)閯?chuàng)建的是臨時(shí) znode,只要客戶端掛了,znode 就沒了,此時(shí)就自動(dòng)釋放鎖。
redis 分布式鎖大家沒發(fā)現(xiàn)好麻煩嗎?遍歷上鎖,計(jì)算時(shí)間等等......zk 的分布式鎖語義清晰實(shí)現(xiàn)簡單。
所以先不分析太多的東西,就說這兩點(diǎn),我個(gè)人實(shí)踐認(rèn)為 zk 的分布式鎖比 redis 的分布式鎖牢靠、而且模型簡單易用。
需要更多java架構(gòu)資料及面試真題請關(guān)注私信回復(fù) 666?