分布式系統的面試題12
1、面試題
一般實現分布式鎖都有哪些方式?使用redis如何設計分布式鎖?使用zk來設計分布式鎖可以嗎?這兩種分布式鎖的實現方式哪種效率比較高?
2、面試官心裏分析
其實一般問問題,都是這麽問的,先問問你zk,然後其實是要過度的zk關聯的一些問題裏去,比如分布式鎖。因為在分布式系統開發中,分布式鎖的使用場景還是很常見的。
3、面試題剖析
(1)redis分布式鎖
官方叫做RedLock算法,是redis官方支持的分布式鎖算法。
這個分布式鎖有3個重要的考量點,互斥(只能有一個客戶端獲取鎖),不能死鎖,容錯(大部分redis節點或者這個鎖就可以加可以釋放)
第一個最普通的實現方式,如果就是在redis裏創建一個key算加鎖
SET my:lock 隨機值 NX PX 30000,這個命令就ok,這個的NX的意思就是只有key不存在的時候才會設置成功,PX 30000的意思是30秒後鎖自動釋放。別人創建的時候如果發現已經有了就不能加鎖了。
釋放鎖就是刪除key,但是一般可以用lua腳本刪除,判斷value一樣才刪除:
關於redis如何執行lua腳本,自行百度
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return0 end
為啥要用隨機值呢?因為如果某個客戶端獲取到了鎖,但是阻塞了很長時間才執行完,此時可能已經自動釋放鎖了,此時可能別的客戶端已經獲取到了這個鎖,要是你這個時候直接刪除key的話會有問題,所以得用隨機值加上面的lua腳本來釋放鎖。
但是這樣是肯定不行的。因為如果是普通的redis單實例,那就是單點故障。或者是redis普通主從,那redis主從異步復制,如果主節點掛了,key還沒同步到從節點,此時從節點切換為主節點,別人就會拿到鎖。
第二個問題,RedLock算法
這個場景是假設有一個redis cluster,有5個redis master實例。然後執行如下步驟獲取一把鎖:
1)獲取當前時間戳,單位是毫秒
2)跟上面類似,輪流嘗試在每個master節點上創建鎖,過期時間較短,一般就幾十毫秒
3)嘗試在大多數節點上建立一個鎖,比如5個節點就要求是3個節點(n / 2 +1)
4)客戶端計算建立好鎖的時間,如果建立鎖的時間小於超時時間,就算建立成功了
5)要是鎖建立失敗了,那麽就依次刪除這個鎖
6)只要別人建立了一把分布式鎖,你就得不斷輪詢去嘗試獲取鎖
(2)zk分布式鎖
zk分布式鎖,其實可以做的比較簡單,就是某個節點嘗試創建臨時znode,此時創建成功了就獲取了這個鎖;這個時候別的客戶端來創建鎖會失敗,只能註冊個監聽器監聽這個鎖。釋放鎖就是刪除這個znode,一旦釋放掉就會通知客戶端,然後有一個等待著的客戶端就可以再次重新枷鎖。
/** * ZooKeeperSession * @author Administrator * */ 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 { Stat stat = zk.exists(path, true); // 相當於是給node註冊一個監聽器,去看看這個監聽器是否存在 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 e) { continue; } } // 很不優雅,我呢就是給大家來演示這麽一個思路 // 比較通用的,我們公司裏我們自己封裝的基於zookeeper的分布式鎖,我們基於zookeeper的臨時順序節點去實現的,比較優雅的 } 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 Administrator * */ 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(); } } } /** * 封裝單例的靜態內部類 * @author Administrator * */ 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(); } }
(3)redis分布式鎖和zk分布式鎖的對比
redis分布式鎖,其實需要自己不斷去嘗試獲取鎖,比較消耗性能
zk分布式鎖,獲取不到鎖,註冊個監聽器即可,不需要不斷主動嘗試獲取鎖,性能開銷較小
另外一點就是,如果是redis獲取鎖的那個客戶端bug了或者掛了,那麽只能等待超時時間之後才能釋放鎖;而zk的話,因為創建的是臨時znode,只要客戶端掛了,znode就沒了,此時就自動釋放鎖
redis分布式鎖大家每發現好麻煩嗎?遍歷上鎖,計算時間等等。。。zk的分布式鎖語義清晰實現簡單
所以先不分析太多的東西,就說這兩點,我個人實踐認為zk的分布式鎖比redis的分布式鎖牢靠、而且模型簡單易用。
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 // 假設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); // 看看剛創建的節點是不是最小的節點 // locks:10000000000,10000000001,10000000002 List<String> locks = zk.getChildren(locksRoot, false); Collections.sort(locks); if(lockNode.equals(locksRoot+"/"+ locks.get(0))){ //如果是最小的節點,則表示取得鎖 return true; } //如果不是最小的節點,找到比自己小1的節點 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節點 // 刪除/locks/10000000001節點 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); } } // 如果有一把鎖,被多個人給競爭,此時多個人會排隊,第一個拿到鎖的人會執行,然後釋放鎖,後面的每個人都會去監聽排在自己前面的那個人創建的node上,一旦某個人釋放了鎖,排在自己後面的人就會被zookeeper給通知,一旦被通知了之後,就ok了,自己就獲取到了鎖,就可以執行代碼了 }
分布式系統的面試題12