面試原題
一般實現(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?