多線程狀況下對共享資源的操做須要加鎖,避免數據被寫亂,在分佈式系統中,這個問題也是存在的,此時就須要一個分佈式鎖服務。常見的分佈式鎖實現通常是基於DB、Redis、zookeeper。下面筆者會按照順序分析下這3種分佈式鎖的設計與實現,想直接看分佈式鎖總結的小夥伴可直接翻到文檔末尾處。html
分佈式鎖的實現由多種方式,可是無論怎樣,分佈式鎖通常要有如下特色:node
除了以上特色以外,分佈式鎖最好也能知足可重入、高性能、阻塞鎖特性(AQS這種,可以及時從阻塞狀態喚醒)等,下面就話很少說,趕忙上(開往分佈式鎖的設計與實現的)車~redis
DB鎖sql
在數據庫新建一張表用於控制併發控制,表結構能夠以下所示:數據庫
CREATE TABLE `lock_table` ( `id` int(11) unsigned NOT NULL COMMENT '主鍵', `key_id` bigint(20) NOT NULL COMMENT '分佈式key', `memo` varchar(43) NOT NULL DEFAULT '' COMMENT '可記錄操做內容', `update_time` datetime NOT NULL COMMENT '更新時間', PRIMARY KEY (`id`,`key_id`), UNIQUE KEY `key_id` (`key_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
key_id做爲分佈式key用來併發控制,memo可用來記錄一些操做內容(好比memo可用來支持重入特性,標記下當前加鎖的client和加鎖次數)。將key_id設置爲惟一索引,保證了針對同一個key_id只有一個加鎖(數據插入)能成功。此時lock和unlock僞代碼以下:apache
def lock : exec sql: insert into lock_table(key_id, memo, update_time) values (key_id, memo, NOW()) if result == true : return true else : return false def unlock : exec sql: delete from lock_table where key_id = 'key_id' and memo = 'memo'
注意,僞代碼中的lock操做是非阻塞鎖,也就是tryLock,若是想實現阻塞(或者阻塞超時)加鎖,只修反覆執行lock僞代碼直到加鎖成功爲止便可。基於DB的分佈式鎖其實有一個問題,那就是若是加鎖成功後,client端宕機或者因爲網絡緣由致使沒有解鎖,那麼其餘client就沒法對該key_id進行加鎖而且沒法釋放了。爲了可以讓鎖失效,須要在應用層加上定時任務,去刪除過時還未解鎖的記錄,好比刪除2分鐘前未解鎖的僞代碼以下:api
def clear_timeout_lock : exec sql : delete from lock_table where update_time < ADDTIME(NOW(),'-00:02:00')
由於單實例DB的TPS通常爲幾百,因此基於DB的分佈式性能上限通常也是1k如下,通常在併發量不大的場景下該分佈式鎖是知足需求的,不會出現性能問題。不過DB做爲分佈式鎖服務須要考慮單點問題,對於分佈式系統來講是不容許出現單點的,通常經過數據庫的同步複製,以及使用vip切換Master就能解決這個問題。緩存
以上DB分佈式鎖是經過insert來實現的,若是加鎖的數據已經在數據庫中存在,那麼用select xxx where key_id = xxx for udpate方式來作也是能夠的。安全
順便在此給你們推薦一個Java架構方面的交流學習羣:698581634,進羣便可免費獲取Java架構學習資料:裏面有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系,羣裏必定有你須要的資料,你們趕忙加羣吧。性能優化
Redis鎖
Redis鎖是經過如下命令對資源進行加鎖:
set key_id key_value NX PX expireTime
其中,set nx命令只會在key不存在時給key進行賦值,px用來設置key過時時間,key_value通常是隨機值,用來保證釋放鎖的安全性(釋放時會判斷是不是以前設置過的隨機值,只有是才釋放鎖)。因爲資源設置了過時時間,必定時間後鎖會自動釋放。
set nx保證併發加鎖時只有一個client能設置成功(Redis內部是單線程,而且數據存在內存中,也就是說redis內部執行命令是不會有多線程同步問題的),此時的lock/unlock僞代碼以下:
def lock: if (redis.call('set', KEYS[1], ARGV[1], 'ex', ARGV[2], 'nx')) then return true end return false def unlock: if (redis.call('get', KEYS[1]) == ARGV[1]) then redis.call('del', KEYS[1]) return true end return false
分佈式鎖服務中的一個問題
若是一個獲取到鎖的client由於某種緣由致使沒能及時釋放鎖,而且redis由於超時釋放了鎖,另一個client獲取到了鎖,此時狀況以下圖所示:
那麼如何解決這個問題呢,一種方案是引入鎖續約機制,也就是獲取鎖以後,釋放鎖以前,會定時進行鎖續約,好比以鎖超時時間的1/3爲間隔週期進行鎖續約。
關於開源的redis的分佈式鎖實現有不少,比較出名的有 redisson 、百度的 dlock ,關於分佈式鎖,筆者也寫了一個簡易版的分佈式鎖 redis-lock ,主要是增長了鎖續約和可同時針對多個key加鎖的機制。
對於高可用性,通常能夠經過集羣或者master-slave來解決,redis鎖優點是性能出色,劣勢就是因爲數據在內存中,一旦緩存服務宕機,鎖數據就丟失了。像redis自帶複製功能,能夠對數據可靠性有必定的保證,可是因爲複製也是異步完成的,所以依然可能出現master節點寫入鎖數據而未同步到slave節點的時候宕機,鎖數據丟失問題。
順便在此給你們推薦一個Java架構方面的交流學習羣:698581634,進羣便可免費獲取Java架構學習資料:裏面有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系,羣裏必定有你須要的資料,你們趕忙加羣吧。
zookeeper分佈式鎖
ZooKeeper是一個高可用的分佈式協調服務,由雅虎建立,是Google Chubby的開源實現。ZooKeeper提供了一項基本的服務:分佈式鎖服務。zookeeper重要的3個特徵是:zab協議、node存儲模型和watcher機制。經過zab協議保證數據一致性,zookeeper集羣部署保證可用性,node存儲在內存中,提升了數據操做性能,使用watcher機制,實現了通知機制(好比加鎖成功的client釋放鎖時能夠通知到其餘client)。
zookeeper node模型支持臨時節點特性,即client寫入的數據時臨時數據,當客戶端宕機時臨時數據會被刪除,這樣就不須要給鎖增長超時釋放機制了。當針對同一個path併發多個建立請求時,只有一個client能建立成功,這個特性用來實現分佈式鎖。注意:若是client端沒有宕機,因爲網絡緣由致使zookeeper服務與client心跳失敗,那麼zookeeper也會把臨時數據給刪除掉的,這時若是client還在操做共享數據,是有必定風險的。
基於zookeeper實現分佈式鎖,相對於基於redis和DB的實現來講,使用上更容易,效率與穩定性較好。curator封裝了對zookeeper的api操做,同時也封裝了一些高級特性,如:Cache事件監聽、選舉、分佈式鎖、分佈式計數器、分佈式Barrier等,使用curator進行分佈式加鎖示例以下:
<!--引入依賴--> <!--對zookeeper的底層api的一些封裝--> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>2.12.0</version> </dependency> <!--封裝了一些高級特性,如:Cache事件監聽、選舉、分佈式鎖、分佈式計數器、分佈式Barrier等--> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.12.0</version> </dependency> public static void main(String[] args) throws Exception { String lockPath = "/curator_recipes_lock_path"; CuratorFramework client = CuratorFrameworkFactory.builder().connectString("192.168.193.128:2181") .retryPolicy(new ExponentialBackoffRetry(1000, 3)).build(); client.start(); InterProcessMutex lock = new InterProcessMutex(client, lockPath); Runnable task = () -> { try { lock.acquire(); try { System.out.println("zookeeper acquire success: " + Thread.currentThread().getName()); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } finally { lock.release(); } } catch (Exception ex) { ex.printStackTrace(); } }; ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 1000; i++) { executor.execute(task); } LockSupport.park(); }
總結
從上面介紹的3種分佈式鎖的設計與實現中,咱們能夠看出每種實現都有各自的特色,針對潛在的問題有不一樣的解決方案,概括以下:
使用分佈式鎖,安全性上和多線程(同一個進程內)加鎖是無法比的,可能因爲網絡緣由,分佈式鎖服務(由於超時或者認爲client掛了)將加鎖資源給刪除了,若是client端繼續操做共享資源,此時是有隱患的。所以,對於分佈式鎖,一個是儘可能提升分佈式鎖服務的可用性,另外一個就是要部署同一內網,儘可能下降網絡問題發生概率。這樣來看,貌似分佈式鎖服務不是「完美」的(PS:技術貌似也很差作到十全十美 :( ),那麼開發人員該如何選擇分佈式鎖呢?最好是結合本身的業務實際場景,來選擇不一樣的分佈式鎖實現,通常來講,基於redis的分佈式鎖服務應用較多。
原文連接:http://www.cnblogs.com/xiangnanl/p/9833965.html?utm_source=tuicool&utm_medium=referral