分佈式鎖的幾種實現方式

1、爲何要使用分佈式鎖

爲了保證一個方法或屬性在高併發狀況下的同一時間只能被同一個線程執行,在傳統單體應用單機部署的狀況下,可使用Java併發處理相關的API(如ReentrantLock或Synchronized)進行互斥控制。可是,隨着業務發展的須要,原單體單機部署的系統被演化成分佈式集羣系統後,因爲分佈式系統多線程、多進程而且分佈在不一樣機器上,這將使原單機部署狀況下的併發控制鎖策略失效,單純的Java API並不能提供分佈式鎖的能力。爲了解決這個問題就須要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分佈式鎖要解決的問題!java

2、分佈式鎖應該具有哪些條件

在分析分佈式鎖的三種實現方式以前,先了解一下分佈式鎖應該具有哪些條件:
一、在分佈式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行;
二、高可用的獲取鎖與釋放鎖;
三、高性能的獲取鎖與釋放鎖;
四、具有可重入特性;
五、具有鎖失效機制,防止死鎖;
六、具有非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。web

3、分佈式鎖的三種實現方式

目前幾乎不少大型網站及應用都是分佈式部署的,分佈式場景中的數據一致性問題一直是一個比較重要的話題。分佈式的CAP理論告訴咱們「任何一個分佈式系統都沒法同時知足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時知足兩項。」因此,不少系統在設計之初就要對這三者作出取捨。在互聯網領域的絕大多數的場景中,都須要犧牲強一致性來換取系統的高可用性,系統每每只須要保證「最終一致性」,只要這個最終時間是在用戶能夠接受的範圍內便可。redis

在不少場景中,咱們爲了保證數據的最終一致性,須要不少的技術方案來支持,好比分佈式事務、分佈式鎖等。有的時候,咱們須要保證一個方法在同一時間內只能被同一個線程執行。
基於數據庫實現分佈式鎖;
基於緩存(Redis等)實現分佈式鎖;
基於Zookeeper實現分佈式鎖;
儘管有這三種方案,可是不一樣的業務也要根據本身的狀況進行選型,他們之間沒有最好只有更適合!算法

1. 基於數據庫的實現方式

基於數據庫的實現方式的核心思想是:在數據庫中建立一個表,表中包含方法名等字段,並在方法名字段上建立惟一索引,想要執行某個方法,就使用這個方法名向表中插入數據,成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖。sql

1.1 建立一個表:數據庫

DROP TABLE IF EXISTS `method_lock`; CREATE TABLE `method_lock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `method_name` varchar(64) NOT NULL COMMENT '鎖定的方法名', `desc` varchar(255) NOT NULL COMMENT '備註信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法'; DROP TABLE IF EXISTS `method_lock`; CREATE TABLE `method_lock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `method_name` varchar(64) NOT NULL COMMENT '鎖定的方法名', `desc` varchar(255) NOT NULL COMMENT '備註信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';

1.2 想要執行某個方法,就使用這個方法名向表中插入數據:緩存

INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '測試的methodName'); INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '測試的methodName');

由於咱們對method_name作了惟一性約束,這裏若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功,那麼咱們就能夠認爲操做成功的那個線程得到了該方法的鎖,能夠執行方法體內容。多線程

1.3 成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖:併發

delete from method_lock where method_name ='methodName'; delete from method_lock where method_name ='methodName';

注意:這只是使用基於數據庫的一種方法,使用數據庫實現分佈式鎖還有不少其餘的玩法!
上面這種簡單的實現有如下幾個問題:分佈式

  1. 這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會致使業務系統不可用。
  2. 這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在數據庫中,其餘線程沒法再得到到鎖。
  3. 這把鎖只能是非阻塞的,由於數據的insert操做,一旦插入失敗就會直接報錯。沒有得到鎖的線程並不會進入排隊隊列,要想再次得到鎖就要再次觸發得到鎖操做。
  4. 這把鎖是非重入的,同一個線程在沒有釋放鎖以前沒法再次得到該鎖。由於數據中數據已經存在了。

固然,咱們也能夠有其餘方式解決上面的問題。

  1. 數據庫是單點?搞兩個數據庫,數據以前雙向同步。一旦掛掉快速切換到備庫上。
  2. 沒有失效時間?只要作一個定時任務,每隔必定時間把數據庫中的超時數據清理一遍。
  3. 非阻塞的?搞一個while循環,直到insert成功再返回成功。
  4. 非重入的?在數據庫表中加個字段,記錄當前得到鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,若是當前機器的主機信息和線程信息在數據庫能夠查到的話,直接把鎖分配給他就能夠了。

數據庫實現分佈式鎖的優勢:直接藉助數據庫,容易理解。

數據庫實現分佈式鎖的缺點:

  1. 會有各類各樣的問題,在解決問題的過程當中會使整個方案變得愈來愈複雜。
  2. 操做數據庫須要必定的開銷,性能問題須要考慮。
  3. 使用數據庫的行級鎖並不必定靠譜,尤爲是當咱們的鎖表並不大的時候。

2. 基於Redis的實現方式

選用Redis實現分佈式鎖緣由:
(1)Redis有很高的性能;
(2)Redis命令對此支持較好,實現起來比較方便

緩存系統在實現的時候跟數據庫的模式差很少,可是由於數據都是在緩存中,因此加鎖和解鎖都會比數據庫快不少。

下面舉例看看基於 Redis 的分佈式鎖實現。Redis 的分佈式鎖都是基於一個命令 – SETNX,也就是 SET IF NOT EXIST,若是不存在就寫入。從 Redis 2.6.12 版本開始,Redis 的 SET 命令直接直接設置 NX 和 EX 屬性,NX 即附帶了 SETNX 數據,key 存在就沒法插入,EX 是過時屬性,能夠設置過時時間。這樣一個命令就能原子的完成加鎖和設置過時時間。

pom文件是這樣。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>
public class RedisManager { public static JedisPool jedisPool; private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; private static final Long RELEASE_SUCCESS = 1L; /** * * 過時時間設置 * EX second :設置鍵的過時時間爲 second 秒。 SET key value EX second 效果等同於 SETEX key second value 。 * PX millisecond :設置鍵的過時時間爲 millisecond 毫秒。 SET key value PX millisecond 效果等同於 PSETEX key millisecond value 。 * * 執行條件設置 * NX :只在鍵不存在時,纔對鍵進行設置操做。 SET key value NX 效果等同於 SETNX key value 。 * XX :只在鍵已經存在時,纔對鍵進行設置操做。 */ static { //讀取相關的配置 ResourceBundle resourceBundle = ResourceBundle.getBundle("redis"); int maxActive = Integer.parseInt(resourceBundle.getString("redis.pool.maxActive")); int maxIdle = Integer.parseInt(resourceBundle.getString("redis.pool.maxIdle")); int maxWait = Integer.parseInt(resourceBundle.getString("redis.pool.maxWait")); String ip = resourceBundle.getString("redis.ip"); int port = Integer.parseInt(resourceBundle.getString("redis.port")); JedisPoolConfig config = new JedisPoolConfig(); //設置最大鏈接數 config.setMaxTotal(maxActive); //設置最大空閒數 config.setMaxIdle(maxIdle); //設置超時時間 config.setMaxWaitMillis(maxWait); //初始化鏈接池 jedisPool = new JedisPool(config, ip, port); } public static boolean tryLock(String key,String value,int expireSecond){ Jedis jedis = jedisPool.getResource(); if(jedis == null){ return false; } String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireSecond); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } public static boolean releaseDistributedLock(String key,String value) { Jedis jedis = jedisPool.getResource(); if(jedis == null){ return false; } String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } public static void main(String[] args){ Printer.println(tryLock("A","B",100)); Printer.println(releaseDistributedLock("A","B")); } } public class RedisManager { public static JedisPool jedisPool; private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; private static final Long RELEASE_SUCCESS = 1L; /** * * 過時時間設置 * EX second :設置鍵的過時時間爲 second 秒。 SET key value EX second 效果等同於 SETEX key second value 。 * PX millisecond :設置鍵的過時時間爲 millisecond 毫秒。 SET key value PX millisecond 效果等同於 PSETEX key millisecond value 。 * * 執行條件設置 * NX :只在鍵不存在時,纔對鍵進行設置操做。 SET key value NX 效果等同於 SETNX key value 。 * XX :只在鍵已經存在時,纔對鍵進行設置操做。 */ static { //讀取相關的配置 ResourceBundle resourceBundle = ResourceBundle.getBundle("redis"); int maxActive = Integer.parseInt(resourceBundle.getString("redis.pool.maxActive")); int maxIdle = Integer.parseInt(resourceBundle.getString("redis.pool.maxIdle")); int maxWait = Integer.parseInt(resourceBundle.getString("redis.pool.maxWait")); String ip = resourceBundle.getString("redis.ip"); int port = Integer.parseInt(resourceBundle.getString("redis.port")); JedisPoolConfig config = new JedisPoolConfig(); //設置最大鏈接數 config.setMaxTotal(maxActive); //設置最大空閒數 config.setMaxIdle(maxIdle); //設置超時時間 config.setMaxWaitMillis(maxWait); //初始化鏈接池 jedisPool = new JedisPool(config, ip, port); } public static boolean tryLock(String key,String value,int expireSecond){ Jedis jedis = jedisPool.getResource(); if(jedis == null){ return false; } String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireSecond); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } public static boolean releaseDistributedLock(String key,String value) { Jedis jedis = jedisPool.getResource(); if(jedis == null){ return false; } String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } public static void main(String[] args){ Printer.println(tryLock("A","B",100)); Printer.println(releaseDistributedLock("A","B")); } }

除此以外,Redis 的做者還實現了一個分佈式鎖算法,叫Redlock
以上實現方式一樣存在幾個問題:

  1. 這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在tair中,其餘線程沒法再得到到鎖。
  2. 這把鎖只能是非阻塞的,不管成功仍是失敗都直接返回。
  3. 這把鎖是非重入的,一個線程得到鎖以後,在釋放鎖以前,沒法再次得到該鎖,由於使用到的key在tair中已經存在。沒法再執行put操做。

固然,一樣有方式能夠解決。

  1. 沒有失效時間?tair的put方法支持傳入失效時間,到達時間以後數據會自動刪除。
  2. 非阻塞?while重複執行。
  3. 非可重入?在一個線程獲取到鎖以後,把當前主機信息和線程信息保存起來,下次再獲取以前先檢查本身是否是當前鎖的擁有者。

可是,失效時間我設置多長時間爲好?如何設置的失效時間過短,方法沒等執行完,鎖就自動釋放了,那麼就會產生併發問題。若是設置的時間太長,其餘獲取鎖的線程就可能要平白的多等一段時間。這個問題使用數據庫實現分佈式鎖一樣存在

總結
可使用緩存來代替數據庫來實現分佈式鎖,這個能夠提供更好的性能,同時,不少緩存服務都是集羣部署的,能夠避免單點問題。而且不少緩存服務都提供了能夠用來實現分佈式鎖的方法,好比Tair的put方法,redis的setnx方法等。而且,這些緩存服務也都提供了對數據的過時自動刪除的支持,能夠直接設置超時時間來控制鎖的釋放。

使用緩存實現分佈式鎖的優勢

  1. 性能好,實現起來較爲方便。
  2. 使用緩存實現分佈式鎖的缺點
  3. 經過超時時間來控制鎖的失效時間並非十分的靠譜。

3. 基於ZooKeeper的實現方式

ZooKeeper是一個爲分佈式應用提供一致性服務的開源組件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個惟一文件名。基於ZooKeeper實現分佈式鎖的步驟以下:

  1. 建立一個目錄mylock;
  2. 線程A想獲取鎖就在mylock目錄下建立臨時順序節點;
  3. 獲取mylock目錄下全部的子節點,而後獲取比本身小的兄弟節點,若是不存在,則說明當前線程順序號最小,得到鎖;
  4. 線程B獲取全部節點,判斷本身不是最小節點,設置監聽比本身次小的節點;
  5. 線程A處理完,刪除本身的節點,線程B監聽到變動事件,判斷本身是否是最小的節點,若是是則得到鎖。

這裏推薦一個Apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分佈式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。
優勢:具有高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。
缺點:由於須要頻繁的建立和刪除節點,性能上不如Redis方式。

4. 總結

上面的三種實現方式,沒有在全部場合都是完美的,因此,應根據不一樣的應用場景選擇最適合的實現方式。

在分佈式環境中,對資源進行上鎖有時候是很重要的,好比搶購某一資源,這時候使用分佈式鎖就能夠很好地控制資源。
固然,在具體使用中,還須要考慮不少因素,好比超時時間的選取,獲取鎖時間的選取對併發量都有很大的影響

參考:
http://www.hollischuang.com/archives/1716
https://blog.csdn.net/xlgen157387/article/details/79036337
https://toutiao.io/posts/8vnqlo/preview

相關文章
相關標籤/搜索