分佈式鎖概念及實現方式

分佈式鎖概念

什麼是鎖?

  • 在單進程的系統中,當存在多個線程能夠同時改變某個變量(可變共享變量)時,就須要對變量或代碼塊作同步,使其在修改這種變量時可以線性執行,以防止併發修改變量帶來不可控的結果。
  • 同步的本質是經過鎖來實現的。爲了實現多個線程在一個時刻同一個代碼塊只能有一個線程可執行,那麼須要在某個地方作個標記,這個標記必須每一個線程都能看到,當標記不存在時能夠設置該標記,其他後續線程發現已經有標記了則等待擁有標記的線程結束同步代碼塊取消標記後再去嘗試設置標記。這個標記能夠理解爲鎖。
  • 不一樣地方實現鎖的方式也不同,只要能知足全部線程都能看獲得標記便可。如 Java 中 synchronize 是在對象頭設置標記,Lock 接口的實現類基本上都只是某一個 volitile 修飾的 int 型變量其保證每一個線程都能擁有對該 int 的可見性和原子修改,linux 內核中也是利用互斥量或信號量等內存數據作標記。
  • 除了利用內存數據作鎖其實任何互斥的都能作鎖(只考慮互斥狀況),如流水錶中流水號與時間結合作冪等校驗能夠看做是一個不會釋放的鎖,或者使用某個文件是否存在做爲鎖等。只須要知足在對標記進行修改能保證原子性和內存可見性便可。

什麼是分佈式鎖?

分佈式鎖是控制分佈式系統同步訪問共享資源的一種方式。java

分佈式鎖應該有什麼特性?

一、在分佈式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行; 二、高可用的獲取鎖與釋放鎖; 三、高性能的獲取鎖與釋放鎖; 四、具有可重入特性; 五、具有鎖失效機制,防止死鎖; 六、具有非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。node

分佈式鎖的幾種實現方式

目前分佈式鎖的實現方式主要採用如下三種:linux

  1. 基於數據庫實現分佈式鎖
  2. 基於緩存(Redis等)實現分佈式鎖
  3. 基於Zookeeper實現分佈式鎖

儘管有這三種方案,可是不一樣的業務也要根據本身的狀況進行選型,他們之間沒有最好只有更適合!git

基於數據庫實現分佈式鎖:

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

1.建立一個表:redis

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='鎖定中的方法';
複製代碼

2.若是要執行某個方法,則使用這個方法名向數據庫總插入數據:算法

INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '測試的methodName');
複製代碼

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

3.成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖:shell

delete from method_lock where method_name ='methodName';
複製代碼

注意:這只是使用基於數據庫的一種方法,使用數據庫實現分佈式鎖還有不少其餘的玩法!數據庫

使用基於數據庫的這種實現方式很簡單,可是對於分佈式鎖應該具有的條件來講,它有一些問題須要解決及優化:

一、由於是基於數據庫實現的,數據庫的可用性和性能將直接影響分佈式鎖的可用性及性能,因此,數據庫須要雙機部署、數據同步、主備切換;

二、不具有可重入的特性,由於同一個線程在釋放鎖以前,行數據一直存在,沒法再次成功插入數據,因此,須要在表中新增一列,用於記錄當前獲取到鎖的機器和線程信息,在再次獲取鎖的時候,先查詢表中機器和線程信息是否和當前機器和線程相同,若相同則直接獲取鎖;

三、沒有鎖失效機制,由於有可能出現成功插入數據後,服務器宕機了,對應的數據沒有被刪除,當服務恢復後一直獲取不到鎖,因此,須要在表中新增一列,用於記錄失效時間,而且須要有定時任務清除這些失效的數據;

四、不具有阻塞鎖特性,獲取不到鎖直接返回失敗,因此須要優化獲取邏輯,循環屢次去獲取。

五、在實施的過程當中會遇到各類不一樣的問題,爲了解決這些問題,實現方式將會愈來愈複雜;依賴數據庫須要必定的資源開銷,性能問題須要考慮。

基於redis實現分佈式鎖:

一、選用Redis實現分佈式鎖緣由:

(1)Redis有很高的性能; (2)Redis命令對此支持較好,實現起來比較方便

二、實現思想:

(1)獲取鎖的時候,使用setnx加鎖,並使用expire命令爲鎖添加一個超時時間,超過該時間則自動釋放鎖,鎖的value值爲一個隨機生成的UUID,經過此在釋放鎖的時候進行判斷。

(2)獲取鎖的時候還設置一個獲取的超時時間,若超過這個時間則放棄獲取鎖。

(3)釋放鎖的時候,經過UUID判斷是否是該鎖,如果該鎖,則執行delete進行鎖釋放。

三、使用命令介紹:

SETNX:

SETNX key val:#當且僅當key不存在時,set一個key爲val的字符串,返回1;若key存在,則什麼都不作,返回0。
複製代碼

EXPIRE:

expire key timeout:#爲key設置一個超時時間,單位爲second,超過這個時間鎖會自動釋放,避免死鎖。
複製代碼

DELETE:

delete key:#刪除key
複製代碼

若是在 setnx 和 expire 之間服務器進程忽然掛掉了,多是由於機器掉電或者是被人爲殺掉的,就會致使 expire 得不到執行,也會形成死鎖。因此可使用如下指令使得setnx和expire在同一條指令中執行:

set lock:codehole value ex 5 nx
複製代碼

四、實現代碼:

//可重入鎖
public class RedisWithReentrantLock {

  private ThreadLocal<Map<String, Integer>> lockers = new ThreadLocal<>();

  private Jedis jedis;

  public RedisWithReentrantLock(Jedis jedis) {
    this.jedis = jedis;
  }

  private boolean _lock(String key) {
    return jedis.set(key, "", "nx", "ex", 5L) != null;
  }

  private void _unlock(String key) {
    jedis.del(key);
  }

  private Map<String, Integer> currentLockers() {
    Map<String, Integer> refs = lockers.get();
    if (refs != null) {
      return refs;
    }
    lockers.set(new HashMap<>());
    return lockers.get();
  }

  public boolean lock(String key) {
    Map<String, Integer> refs = currentLockers();
    Integer refCnt = refs.get(key);
    if (refCnt != null) {
      refs.put(key, refCnt + 1);
      return true;
    }
    boolean ok = this._lock(key);
    if (!ok) {
      return false;
    }
    refs.put(key, 1);
    return true;
  }

  public boolean unlock(String key) {
    Map<String, Integer> refs = currentLockers();
    Integer refCnt = refs.get(key);
    if (refCnt == null) {
      return false;
    }
    refCnt -= 1;
    if (refCnt > 0) {
      refs.put(key, refCnt);
    } else {
      refs.remove(key);
      this._unlock(key);
    }
    return true;
  }

  public static void main(String[] args) {
    Jedis jedis = new Jedis();
    RedisWithReentrantLock redis = new RedisWithReentrantLock(jedis);
    System.out.println(redis.lock("codehole"));
    System.out.println(redis.lock("codehole"));
    System.out.println(redis.unlock("codehole"));
    System.out.println(redis.unlock("codehole"));
  }

}

/** * 分佈式鎖的簡單實現代碼 * Created by liuyang on 2017/4/20. */
public class DistributedLock {

    private final JedisPool jedisPool;

    public DistributedLock(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    /** * 加鎖 * @param lockName 鎖的key * @param acquireTimeout 獲取超時時間 * @param timeout 鎖的超時時間 * @return 鎖標識 */
    public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
        Jedis conn = null;
        String retIdentifier = null;
        try {
            // 獲取鏈接
            conn = jedisPool.getResource();
            // 隨機生成一個value
            String identifier = UUID.randomUUID().toString();
            // 鎖名,即key值
            String lockKey = "lock:" + lockName;
            // 超時時間,上鎖後超過此時間則自動釋放鎖
            int lockExpire = (int) (timeout / 1000);

            // 獲取鎖的超時時間,超過這個時間則放棄獲取鎖
            long end = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < end) {
                if (jedis.set(key, "", "nx", "ex", 5L) != null) {
                    retIdentifier = identifier;
                    return retIdentifier;
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retIdentifier;
    }

    /** * 釋放鎖 * @param lockName 鎖的key * @param identifier 釋放鎖的標識 * @return */
    public boolean releaseLock(String lockName, String identifier) {
        Jedis conn = null;
        String lockKey = "lock:" + lockName;
        boolean retFlag = false;
        try {
            conn = jedisPool.getResource();
            while (true) {
                // 監視lock,準備開始事務
                conn.watch(lockKey);
                // 經過前面返回的value值判斷是否是該鎖,如果該鎖,則刪除,釋放鎖
                if (identifier.equals(conn.get(lockKey))) {
                    Transaction transaction = conn.multi();
                    transaction.del(lockKey);
                    List<Object> results = transaction.exec();
                    if (results == null) {
                        continue;
                    }
                    retFlag = true;
                }
                conn.unwatch();
                break;
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retFlag;
    }
}
複製代碼

使用這種方式實現分佈式鎖在集羣模式下會有必定的問題,好比在 Sentinel 集羣中,主節點掛掉時,從節點會取而代之,客戶端上卻並無明顯感知。原先第一個客戶端在主節點中申請成功了一把鎖,可是這把鎖尚未來得及同步到從節點,主節點忽然掛掉了。而後從節點變成了主節點,這個新的節點內部沒有這個鎖,因此當另外一個客戶端過來請求加鎖時,當即就批准了。這樣就會致使系統中一樣一把鎖被兩個客戶端同時持有,不安全性由此產生。

爲了解決這個問題,Antirez 發明了 Redlock 算法,加鎖時,它會向過半節點發送 set(key, value, nx=True, ex=xxx) 指令,只要過半節點 set 成功,那就認爲加鎖成功。釋放鎖時,須要向全部節點發送 del 指令。不過 Redlock 算法還須要考慮出錯重試、時鐘漂移等不少細節問題,同時由於 Redlock 須要向多個節點進行讀寫,意味着相比單實例 Redis 性能會降低一些。

基於zookeeper實現的分佈式鎖:

在使用zookeeper實現分佈式鎖的以前,須要先了解zookeeper的兩個特性,第一個是zookeeper的節點類型,第二就是zookeeper的watch機制:

zookeeper的節點類型:

PERSISTENT 持久化節點

PERSISTENT_SEQUENTIAL 順序自動編號持久化節點,這種節點會根據當前已存在的節點數自動加 1

EPHEMERAL 臨時節點, 客戶端session超時這類節點就會被自動刪除

EPHEMERAL_SEQUENTIAL 臨時自動編號節點

zookeeper的watch機制:

Znode發生變化(Znode自己的增長,刪除,修改,以及子Znode的變化)能夠經過Watch機制通知到客戶端。那麼要實現Watch,就必須實現org.apache.zookeeper.Watcher接口,而且將實現類的對象傳入到能夠Watch的方法中。Zookeeper中全部讀操做(getData(),getChildren(),exists())均可以設置Watch選項。Watch事件具備one-time trigger(一次性觸發)的特性,若是Watch監視的Znode有變化,那麼就會通知設置該Watch的客戶端。

zookeeper實現排他鎖:

定義鎖:

在一般的java併發編程中,有兩種常見的方式能夠用來定義鎖,分別是synchronized機制和JDK5提供的ReetrantLock。然而,在zookeeper中,沒有相似於這樣的API能夠直接使用,而是經過Zookeeper上的數據節點來表示一個鎖,例如/exclusive_lock/lock節點就能夠定義爲一個鎖。

獲取鎖:

在須要獲取排他鎖的時候,全部的客戶端都會試圖經過調用create()接口,在/exclusive_lock節點下建立臨時子節點/exclusive_lock/lock。zookeeper會保證在全部客戶端中,最終只有一個客戶端可以建立成功,那麼就能夠認爲該客戶端得到了鎖。同時,全部沒有得到鎖的客戶端就須要到/exclusive_lock節點上註冊一個子節點變動的Watcher監聽,以便實時監聽到lock節點的變動狀況。

釋放鎖:

在定義鎖部分,咱們已經提到,/exclusive_lock/lock是一個臨時節點,所以在如下兩種狀況下,都有可能釋放鎖。

1.當前獲取鎖的客戶端發生宕機,那麼Zookeeper上的這個臨時節點就會被移除。

2.正常執行完業務邏輯以後,客戶端就會主動將本身建立的臨時節點刪除

不管在什麼狀況下移除了lock節點,Zookeeper都會通知全部在/exclusive_lock節點上註冊了子節點變動Watcher監聽的客戶端。這些客戶端在接收到通知後,再次從新發起分佈式鎖獲取,即重複「獲取鎖」的過程:

zookeeper實現共享鎖:

定義鎖:

和排他鎖同樣,一樣是經過zookeeper上的數據節點來表示一個鎖,是一個相似於"/shared_lock/[hostname]-請求類型-序號"的臨時順序節點,例如/shared_lock/192.168.0.1-R-000000001,那麼這個節點就表明了一個共享鎖。

獲取鎖:

1.客戶端調用create()方法建立一個相似於"/shared_lock/[hostname]-請求類型-序號"的臨時順序節點。

2.客戶端調用getChildren()接口來獲取全部已經建立的子節點列表。

3.肯定本身的節點序號在全部子節點中的順序

​ 對於讀請求:

​ 若是沒有比本身序號小的子節點,或是全部比本身序號小的 子節點都是讀請求,那麼代表已經成功獲取到了共享鎖,同時開始執行讀取邏輯。

​ 若是比本身序號小的子節點中有寫請求,那麼就須要進入等待。向比本身序號小的最後一個寫請求節點註冊Watcher監聽。

​ 對於寫請求:

​ 若是本身不是序號最小的節點,那麼就須要進入等待。向比本身序號小的最後一個節點註冊Watcher監聽

4.等待Watcher通知,繼續進入步驟2

釋放鎖:

在定義鎖部分,咱們已經提到,/exclusive_lock/lock是一個臨時節點,所以在如下兩種狀況下,都有可能釋放鎖。

1.當前獲取鎖的客戶端發生宕機,那麼Zookeeper上的這個臨時節點就會被移除。

2.正常執行完業務邏輯以後,客戶端就會主動將本身建立的臨時節點刪除

經常使用的分佈式鎖組件:

mykit-lock

mykit架構中獨立出來的mykit-lock組件,旨在提供高併發架構下分佈式系統的分佈式鎖架構。

GitHub地址:github.com/sunshinelyz…

參考資料:

從PAXOS到ZOOKEEPER分佈式一致性原理和實踐

blog.csdn.net/xlgen157387…

掘金小冊:Redis深度探險:核心原理與應用實踐

blog.csdn.net/tzs_1041218…

相關文章
相關標籤/搜索