分布式鎖2 Java非常用技術(shù)方案探討之ZooKeeper
前言:
由于在平時(shí)的工作中,線上服務(wù)器是分布式多臺(tái)部署的,經(jīng)常會(huì)面臨解決分布式場景下數(shù)據(jù)一致性的問題,那么就要利用分布式鎖來解決這些問題。以自己結(jié)合實(shí)際工作中的一些經(jīng)驗(yàn)和網(wǎng)上看到的一些資料,做一個(gè)講解和總結(jié)。之前我已經(jīng)寫了一篇關(guān)于分布式鎖的文章: 分布式鎖1 Java常用技術(shù)方案 。上一篇文章中主要寫的是在日常項(xiàng)目中,較為常見的幾種實(shí)現(xiàn)分布式鎖的方法。通過這些方法,基本上可以解決我們?nèi)粘9ぷ髦写蟛糠謭鼍跋率褂梅植际芥i的問題。
本篇文章主要是在上一篇文章的基礎(chǔ)上,介紹一些雖然日常工作中不常用或者比較實(shí)現(xiàn)起來比較重,但是可以作為技術(shù)方案學(xué)習(xí)了解一下的分布式鎖方案。希望這篇文章可以方便自己以后查閱,同時(shí)要是能幫助到他人那也是很好的。
===============================================================長長的分割線====================================================================
正文:
第一步,使用zookeeper節(jié)點(diǎn)名稱唯一性,用于分布式鎖:
關(guān)于zookeeper集群的搭建,可以參考我之前寫的一篇文章: ZooKeeper1 利用虛擬機(jī)搭建自己的ZooKeeper集群
zookeeper抽象出來的節(jié)點(diǎn)結(jié)構(gòu)是一個(gè)和文件系統(tǒng)類似的小型的樹狀的目錄結(jié)構(gòu),同時(shí)zookeeper機(jī)制規(guī)定:同一個(gè)目錄下只能有一個(gè)唯一的文件名。例如:我們?cè)趜ookeeper的根目錄下,由兩個(gè)客戶端同時(shí)創(chuàng)建一個(gè)名為/myDistributeLock,只有一個(gè)客戶端可以成功。
上述方案和memcached的add()方法、redis的setnx()方法實(shí)現(xiàn)分布式鎖有著相同的思路。這樣的方案實(shí)現(xiàn)起來如果不考慮搭建和維護(hù)zookeeper集群的成本,由于正確性和可靠性是zookeeper機(jī)制自己保證的,實(shí)現(xiàn)還是比較簡單的。
第二步,使用zookeeper臨時(shí)順序節(jié)點(diǎn),用于分布式鎖:
在討論這套方案之前,我們有必要先“吹毛求疵”般的說明一下使用zookeeper節(jié)點(diǎn)名稱唯一性來做分布式鎖這個(gè)方案的缺點(diǎn)。比如,當(dāng)許多線程在等待一個(gè)鎖時(shí),如果鎖得到釋放的時(shí)候,那么所有客戶端都被喚醒,但是僅僅有一個(gè)客戶端得到鎖。在這個(gè)過程中,大量的線程根本沒有獲得鎖的可能性,但是也會(huì)引起大量的上下文切換,這個(gè)系統(tǒng)開銷也是不小的,對(duì)于這樣的現(xiàn)象有一個(gè)專業(yè)名詞,稱之為“驚群效應(yīng)”。
我們首先說明一下zookeeper的順序節(jié)點(diǎn)、臨時(shí)節(jié)點(diǎn)和watcher機(jī)制:
所謂順序節(jié)點(diǎn),假如我們?cè)?myDisLocks/目錄下創(chuàng)建3個(gè)節(jié)點(diǎn),zookeeper集群會(huì)按照發(fā)起創(chuàng)建的順序來創(chuàng)建節(jié)點(diǎn),節(jié)點(diǎn)分別為/myDisLocks/0000000001、/myDisLocks/0000000002、/myDisLocks/0000000003。
所謂臨時(shí)節(jié)點(diǎn),臨時(shí)節(jié)點(diǎn)由某個(gè)客戶端創(chuàng)建,當(dāng)客戶端與zookeeper集群斷開連接,則該節(jié)點(diǎn)自動(dòng)被刪除。
所謂對(duì)于watcher機(jī)制,大家可以參考Apache ZooKeeper Watcher機(jī)制源碼解釋。當(dāng)然如果你之前不知道watcher機(jī)制是個(gè)什么東東,不建議你直接去看前邊我提供的文章鏈接,這樣你極有可能忘掉我們的討論主線,即分布式鎖的實(shí)現(xiàn)方案,而陷入到watcher機(jī)制的源碼實(shí)現(xiàn)中。所以你也可以先看看下面的具體方案,猜測一下watcher是用來干嘛的,我這里先總結(jié)一句話做個(gè)引子: 所謂watcher機(jī)制,你可以簡單一點(diǎn)兒理解成任何一個(gè)連接zookeeper的客戶端可以通過watcher機(jī)制關(guān)注自己感興趣的節(jié)點(diǎn)的增刪改查,當(dāng)這個(gè)節(jié)點(diǎn)發(fā)生增刪改查的操作時(shí),會(huì)“廣播”自己的消息,所有對(duì)此感興趣的節(jié)點(diǎn)可以在收到這些消息后,根據(jù)自己的業(yè)務(wù)需要執(zhí)行后續(xù)的操作。
具體的使用步驟如下:
1. 每個(gè)業(yè)務(wù)線程調(diào)用create()方法創(chuàng)建名為“/myDisLocks/thread”的節(jié)點(diǎn),需要注意的是,這里節(jié)點(diǎn)的創(chuàng)建類型需要設(shè)置為EPHEMERAL_SEQUENTIAL,即節(jié)點(diǎn)類型為臨時(shí)順序節(jié)點(diǎn)。此時(shí)/myDisLocks節(jié)點(diǎn)下會(huì)出現(xiàn)諸如/myDisLocks/thread0000000001、/myDisLocks/thread0000000002、/myDisLocks/thread0000000003這樣的子節(jié)點(diǎn)。
2. 每個(gè)業(yè)務(wù)線程調(diào)用getChildren(“myDisLocks”)方法來獲取/myDisLocks這個(gè)節(jié)點(diǎn)下所有已經(jīng)創(chuàng)建的子節(jié)點(diǎn)。
3. 每個(gè)業(yè)務(wù)線程獲取到所有子節(jié)點(diǎn)的路徑之后,如果發(fā)現(xiàn)自己在步驟1中創(chuàng)建的節(jié)點(diǎn)的尾綴編號(hào)是所有節(jié)點(diǎn)中序號(hào)最小的,那么就認(rèn)為自己獲得了鎖。
4. 如果在步驟3中發(fā)現(xiàn)自己并非是所有子節(jié)點(diǎn)中序號(hào)最小的,說明自己還沒有獲取到鎖。使用watcher機(jī)制監(jiān)視比自己創(chuàng)建節(jié)點(diǎn)的序列號(hào)小的節(jié)點(diǎn)(比自己創(chuàng)建的節(jié)點(diǎn)小的最大節(jié)點(diǎn)),進(jìn)入等待。比如,如果當(dāng)前業(yè)務(wù)線程創(chuàng)建的節(jié)點(diǎn)是/myDisLocks/thread0000000003,那么在沒有獲取到鎖的情況下,他只需要監(jiān)視/myDisLocks/thread0000000002的情況。只有當(dāng)/myDisLocks/thread0000000002獲取到鎖并釋放之后,當(dāng)前業(yè)務(wù)線程才啟動(dòng)獲取鎖,這樣可以避免一個(gè)業(yè)務(wù)線程釋放鎖之后,其他所有線程都去競爭鎖,引起不必要的上下文切換,最終造成“驚群現(xiàn)象”。
5. 釋放鎖的過程相對(duì)比較簡單,就是刪除自己創(chuàng)建的那個(gè)子節(jié)點(diǎn)即可。
注意: 這個(gè)方案實(shí)現(xiàn)的分布式鎖還帶著一點(diǎn)兒公平鎖的味道!為什么呢?我們?cè)诶妹總€(gè)節(jié)點(diǎn)的序號(hào)進(jìn)行排隊(duì)以此來避免進(jìn)群現(xiàn)象時(shí),實(shí)際上所有業(yè)務(wù)線程獲得鎖的順序就是自己創(chuàng)建節(jié)點(diǎn)的順序,也就是哪個(gè)業(yè)務(wù)線程先來,哪個(gè)就可以最快獲得鎖。
下面貼出我自己實(shí)現(xiàn)的上述方案的代碼:
1. 代碼中有兩個(gè)Java類: MyDistributedLockByZK.java和LockWatcher.java。其中MyDistributedLockByZK.java中的main函數(shù)利用線程池啟動(dòng)5個(gè)線程,以此來模擬多個(gè)業(yè)務(wù)線程競爭鎖的情況;而LockWatcher.java定義分布式鎖和實(shí)現(xiàn)了watcher機(jī)制。
2. 同時(shí),我使用的zookeeper集群是自己以前利用VMWare搭建的集群,所以zookeeper鏈接是192.168.224.170:2181,大家可以根據(jù)替換成自己的zookeeper鏈接即可。
1 public class MyDistributedLockByZK { 2 /** 線程池 **/ 3 private static ExecutorService executorService = null; 4 private static final int THREAD_NUM = 5; 5 private static int threadNo = 0; 6 private static CountDownLatch threadCompleteLatch = new CountDownLatch(THREAD_NUM); 7 8 /** ZK的相關(guān)配置常量 **/ 9 private static final String CONNECTION_STRING = "192.168.224.170:2181"; 10 private static final int SESSION_TIMEOUT = 10000; 11 // 此變量在LockWatcher中也有一個(gè)同名的靜態(tài)變量,正式使用的時(shí)候,提取到常量類中共同維護(hù)即可。 12 private static final String LOCK_ROOT_PATH = "/myDisLocks"; 13 14 public static void main(String[] args) { 15 // 定義線程池 16 executorService = Executors.newFixedThreadPool(THREAD_NUM, new ThreadFactory() { 17 @Override 18 public Thread newThread(Runnable r) { 19 String name = String.format("第[%s]個(gè)測試線程", ++threadNo); 20 Thread ret = new Thread(Thread.currentThread().getThreadGroup(), r, name, 0); 21 ret.setDaemon(false); 22 return ret; 23 } 24 }); 25 26 // 啟動(dòng)線程 27 if (executorService != null) { 28 startProcess(); 29 } 30 } 31 32 /** 33 * @author zhangyi03 34 * @date 2017-5-23 下午5:57:27 35 * @description 模擬并發(fā)執(zhí)行任務(wù) 36 */ 37 public static void startProcess() { 38 Runnable disposeBusinessRunnable= new Thread(new Runnable() { 39 public void run() { 40 String threadName = Thread.currentThread().getName(); 41 42 LockWatcher lock = new LockWatcher(threadCompleteLatch); 43 try { 44 /** 步驟1: 當(dāng)前線程創(chuàng)建ZK連接 **/ 45 lock.createConnection(CONNECTION_STRING, SESSION_TIMEOUT); 46 47 /** 步驟2: 創(chuàng)建鎖的根節(jié)點(diǎn) **/ 48 // 注意,此處創(chuàng)建根節(jié)點(diǎn)的方式其實(shí)完全可以在初始化的時(shí)候由主線程單獨(dú)進(jìn)行根節(jié)點(diǎn)的創(chuàng)建,沒有必要在業(yè)務(wù)線程中創(chuàng)建。 49 // 這里這樣寫只是一種思路而已,不必局限于此 50 synchronized (MyDistributedLockByZK.class){ 51 lock.createPersistentPath(LOCK_ROOT_PATH, "該節(jié)點(diǎn)由" + threadName + "創(chuàng)建", true); 52 } 53 54 /** 步驟3: 開啟鎖競爭并執(zhí)行任務(wù) **/ 55 lock.getLock(); 56 } catch (Exception e) { 57 e.printStackTrace(); 58 } 59 } 60 }); 61 62 for (int i = 0; i < THREAD_NUM; i++) { 63 executorService.execute(disposeBusinessRunnable); 64 } 65 executorService.shutdown(); 66 67 try { 68 threadCompleteLatch.await(); 69 System.out.println("所有線程運(yùn)行結(jié)束!"); 70 } catch (InterruptedException e) { 71 e.printStackTrace(); 72 } 73 } 74 }
1 public class LockWatcher implements Watcher { 2 /** 成員變量 **/ 3 private ZooKeeper zk = null; 4 // 當(dāng)前業(yè)務(wù)線程競爭鎖的時(shí)候創(chuàng)建的節(jié)點(diǎn)路徑 5 private String selfPath = null; 6 // 當(dāng)前業(yè)務(wù)線程競爭鎖的時(shí)候創(chuàng)建節(jié)點(diǎn)的前置節(jié)點(diǎn)路徑 7 private String waitPath = null; 8 // 確保連接zk成功;只有當(dāng)收到Watcher的監(jiān)聽事件之后,才執(zhí)行后續(xù)的操作,否則請(qǐng)求阻塞在createConnection()創(chuàng)建ZK連接的方法中 9 private CountDownLatch connectSuccessLatch = new CountDownLatch(1); 10 // 標(biāo)識(shí)線程是否執(zhí)行完任務(wù) 11 private CountDownLatch threadCompleteLatch = null; 12 13 /** ZK的相關(guān)配置常量 **/ 14 private static final String LOCK_ROOT_PATH = "/myDisLocks"; 15 private static final String LOCK_SUB_PATH = LOCK_ROOT_PATH + "/thread"; 16 17 public LockWatcher(CountDownLatch latch) { 18 this.threadCompleteLatch = latch; 19 } 20 21 @Override 22 public void process(WatchedEvent event) { 23 if (event == null) { 24 return; 25 } 26 27 // 通知狀態(tài) 28 Event.KeeperState keeperState = event.getState(); 29 // 事件類型 30 Event.EventType eventType = event.getType(); 31 32 // 根據(jù)通知狀態(tài)分別處理 33 if (Event.KeeperState.SyncConnected == keeperState) { 34 if ( Event.EventType.None == eventType ) { 35 System.out.println(Thread.currentThread().getName() + "成功連接上ZK服務(wù)器"); 36 // 此處代碼的主要作用是用來輔助判斷當(dāng)前線程確實(shí)已經(jīng)連接上ZK 37 connectSuccessLatch.countDown(); 38 }else if (event.getType() == Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) { 39 System.out.println(Thread.currentThread().getName() + "收到情報(bào),排我前面的家伙已掛,我準(zhǔn)備再次確認(rèn)我是不是最小的節(jié)點(diǎn)?。?); 40 try { 41 if(checkMinPath()){ 42 getLockSuccess(); 43 } 44 } catch (Exception e) { 45 e.printStackTrace(); 46 } 47 } 48 } else if ( Event.KeeperState.Disconnected == keeperState ) { 49 System.out.println(Thread.currentThread().getName() + "與ZK服務(wù)器斷開連接"); 50 } else if ( Event.KeeperState.AuthFailed == keeperState ) { 51 System.out.println(Thread.currentThread().getName() + "權(quán)限檢查失敗"); 52 } else if ( Event.KeeperState.Expired == keeperState ) { 53 System.out.println(Thread.currentThread().getName() + "會(huì)話失效"); 54 } 55 } 56 57 /** 58 * @author zhangyi03 59 * @date 2017-5-23 下午6:07:03 60 * @description 創(chuàng)建ZK連接 61 * @param connectString ZK服務(wù)器地址列表 62 * @param sessionTimeout Session超時(shí)時(shí)間 63 * @throws IOException 64 * @throws InterruptedException 65 */ 66 public void createConnection(String connectString, int sessionTimeout) throws IOException, InterruptedException { 67 zk = new ZooKeeper(connectString, sessionTimeout, this); 68 // connectSuccessLatch.await(1, TimeUnit.SECONDS) 正式實(shí)現(xiàn)的時(shí)候可以考慮此處是否采用超時(shí)阻塞 69 connectSuccessLatch.await(); 70 } 71 72 /** 73 * @author zhangyi03 74 * @date 2017-5-23 下午6:15:48 75 * @description 創(chuàng)建ZK節(jié)點(diǎn) 76 * @param path 節(jié)點(diǎn)path 77 * @param data 初始數(shù)據(jù)內(nèi)容 78 * @param needWatch 79 * @return 80 * @throws KeeperException 81 * @throws InterruptedException 82 */ 83 public boolean createPersistentPath(String path, String data, boolean needWatch) throws KeeperException, InterruptedException { 84 if(zk.exists(path, needWatch) == null){ 85 String result = zk.create( path,data.getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); 86 System.out.println(Thread.currentThread().getName() + "創(chuàng)建節(jié)點(diǎn)成功, path: " + result + ", content: " + data); 87 } 88 return true; 89 } 90 91 /** 92 * @author zhangyi03 93 * @date 2017-5-23 下午6:24:46 94 * @description 獲取分布式鎖 95 * @throws KeeperException 96 * @throws InterruptedException 97 */ 98 public void getLock() throws Exception { 99 selfPath = zk.create(LOCK_SUB_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); 100 System.out.println(Thread.currentThread().getName() + "創(chuàng)建鎖路徑:" + selfPath); 101 if(checkMinPath()){ 102 getLockSuccess(); 103 } 104 } 105 106 /** 107 * @author zhangyi03 108 * @date 2017-5-23 下午7:02:41 109 * @description 獲取鎖成功 110 * @throws KeeperException 111 * @throws InterruptedException 112 */ 113 private void getLockSuccess() throws KeeperException, InterruptedException { 114 if(zk.exists(selfPath, false) == null){ 115 System.err.println(Thread.currentThread().getName() + "本節(jié)點(diǎn)已不在了..."); 116 return; 117 } 118 System.out.println(Thread.currentThread().getName() + "獲取鎖成功,開始處理業(yè)務(wù)數(shù)據(jù)!"); 119 Thread.sleep(2000); 120 System.out.println(Thread.currentThread().getName() + "處理業(yè)務(wù)數(shù)據(jù)完成,刪除本節(jié)點(diǎn):" + selfPath); 121 zk.delete(selfPath, -1); 122 releaseConnection(); 123 threadCompleteLatch.countDown(); 124 } 125 126 /** 127 * @author zhangyi03 128 * @date 2017-5-23 下午7:06:46 129 * @description 關(guān)閉ZK連接 130 */ 131 private void releaseConnection() { 132 if (zk != null) { 133 try { 134 zk.close(); 135 } catch (InterruptedException e) { 136 e.printStackTrace(); 137 } 138 } 139 System.out.println(Thread.currentThread().getName() + "釋放ZK連接"); 140 } 141 142 /** 143 * @author zhangyi03 144 * @date 2017-5-23 下午6:57:14 145 * @description 檢查自己是不是最小的節(jié)點(diǎn) 146 * @param selfPath 147 * @return 148 * @throws KeeperException 149 * @throws InterruptedException 150 */ 151 private boolean checkMinPath() throws Exception { 152 List<String> subNodes = zk.getChildren(LOCK_ROOT_PATH, false); 153 // 根據(jù)元素按字典序升序排序 154 Collections.sort(subNodes); 155 System.err.println(Thread.currentThread().getName() + "創(chuàng)建的臨時(shí)節(jié)點(diǎn)名稱:" + selfPath.substring(LOCK_ROOT_PATH.length()+1)); 156 int index = subNodes.indexOf(selfPath.substring(LOCK_ROOT_PATH.length()+1)); 157 System.err.println(Thread.currentThread().getName() + "創(chuàng)建的臨時(shí)節(jié)點(diǎn)的index:" + index); 158 switch (index){ 159 case -1: { 160 System.err.println(Thread.currentThread().getName() + "創(chuàng)建的節(jié)點(diǎn)已不在了..." + selfPath); 161 return false; 162 } 163 case 0:{ 164 System.out.println(Thread.currentThread().getName() + "子節(jié)點(diǎn)中,我果然是老大" + selfPath); 165 return true; 166 } 167 default:{ 168 // 獲取比當(dāng)前節(jié)點(diǎn)小的前置節(jié)點(diǎn),此處只關(guān)注前置節(jié)點(diǎn)是否還在存在,避免驚群現(xiàn)象產(chǎn)生 169 waitPath = LOCK_ROOT_PATH +"/"+ subNodes.get(index - 1); 170 System.out.println(Thread.currentThread().getName() + "獲取子節(jié)點(diǎn)中,排在我前面的節(jié)點(diǎn)是:" + waitPath); 171 try { 172 zk.getData(waitPath, true, new Stat()); 173 return false; 174 } catch (Exception e) { 175 if (zk.exists(waitPath, false) == null) { 176 System.out.println(Thread.currentThread().getName() + "子節(jié)點(diǎn)中,排在我前面的" + waitPath + "已失蹤,該我了"); 177 return checkMinPath(); 178 } else { 179 throw e; 180 } 181 } 182 } 183 184 } 185 } 186 }
第三步,使用memcached的cas()方法,用于分布式鎖:
下篇文章我們?cè)偌?xì)說!
第四步,使用redis的watch、multi、exec命令,用于分布式鎖:
下篇文章我們?cè)偌?xì)說!
第五步,總結(jié):
綜上,對(duì)于分布式鎖這些非常用或者實(shí)現(xiàn)起來比較重的方案,大家可以根據(jù)自己在項(xiàng)目中的需要,酌情使用。最近在和別人討論的過程中,以及我的第一篇關(guān)于分布式鎖的文章分布式鎖1 Java常用技術(shù)方案 大家的回復(fù)中,總結(jié)來看,對(duì)于用redis實(shí)現(xiàn)分布式鎖確實(shí)存在著比較多的細(xì)節(jié)問題可以進(jìn)行深入討論,歡迎大家留言,相互學(xué)習(xí)。
忍不住嘚瑟一下,我媳婦兒此刻在我旁邊看AbstractQueuedSynchronizer,厲害吧?!,一會(huì)兒出去吃飯,哈哈~
第六步,線上使用補(bǔ)充篇:
截止到2017.08.25(周五),使用上述文章中的”臨時(shí)節(jié)點(diǎn)+watcher機(jī)制方案”解決一個(gè)分布式鎖的問題時(shí),最終發(fā)現(xiàn)在實(shí)現(xiàn)過程中,由于watcher機(jī)制類似于通知等待機(jī)制的特點(diǎn),如果主線程在經(jīng)歷“獲取鎖操作”、“處理業(yè)務(wù)代碼”、“釋放鎖操作”這三步的過程中,使用watcher機(jī)制阻塞的獲取鎖時(shí),會(huì)導(dǎo)致根本無法將獲取鎖結(jié)果返回給主線程,而在實(shí)際的時(shí)候過程中,一般情況下主線程在“獲取鎖操作”時(shí)都希望可以同步獲得一個(gè)返回值。
所以,上述的”臨時(shí)節(jié)點(diǎn)+watcher機(jī)制方案”從技術(shù)方案角度足夠完美,但是在實(shí)際使用過程中,個(gè)人覺得還不是特別的方便。
轉(zhuǎn)載請(qǐng)注明來自博客園http://www.rzrgm.cn/PurpleDream/p/5573040.html ,版權(quán)歸本人和博客園所有,謝謝!
浙公網(wǎng)安備 33010602011771號(hào)