分佈式的幾件小事(十)分佈式鎖是啥

1.什麼是分佈式鎖

分佈式鎖是控制分佈式系統之間同步訪問共享資源的一種方式。在分佈式系統中,經常須要協調他們的動做。若是不一樣的系統或是同一個系統的不一樣主機之間共享了一個或一組資源,那麼訪問這些資源的時候,每每須要互斥來防止彼此干擾來保證一致性,在這種狀況下,便須要使用到分佈式鎖。
分佈式系統中,經常須要協調他們的動做。若是不一樣的系統或是同一個系統的不一樣主機之間共享了一個或一組資源,那麼訪問這些資源的時候,每每須要互斥來防止彼此干擾來保證一致性,這個時候,便須要使用到分佈式鎖。html

2.實現方式

1.基於數據庫實現

①基於數據庫表實現。
實現方式 : 這是最簡單的方法,能夠在數據庫中創建一張表,而後經過操做該表中的數據來實現。
當咱們想要鎖住某個方法或資源的時候,就在表中增長一條記錄,想要釋放鎖的時候就刪除這條記錄。
好比建立以下的表結構:node

CREATE TABLE myLock(
    id INT(11) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵id',
    method_name VARCHAR(256) NOT NULL DEFAULT '' COMMENT '要加鎖的方法名',
    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '數據建立時間',
    PRIMARY key(id),
    UNIQUE key `idx_method_name`(method_name)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='分佈式鎖';

想要鎖住某個方法的時間,就去插入一條數據。mysql

insert into myLock(method_name) values ("method_name")

方法執行完畢以後,就刪除這條數據,釋放鎖。redis

delete from myLock where method_name = "method_name"

存在的問題
A: 強依賴數據庫,若是數據是單點,一旦數據庫掛掉就不可用。
B: 沒有失效時間,一旦解鎖失敗,或者加鎖服務掛掉,鎖將一直存在。
C: 鎖是非阻塞的,引入插入若是失敗叫報錯了,不會等待去獲取鎖,想要從新獲取鎖就要去從新觸發該操做。
D: 鎖是非重入的,同一個線程在釋放鎖以前沒法再次獲取鎖。算法

解決方案
A: 數據庫使用主從架構,進行數據的主從雙向同步,主庫掛掉裏面切換從庫。
B: 能夠在系統裏面作一個定時任務,每隔必定時間就去清理一下超時的鎖。
C: 在進行獲取鎖的時候,在執行insert操做時搞一個while循環不斷去嘗試,知道成功。
D: 能夠在表中再增長信息,好比當前得到鎖的機器信息和線程信息,判斷若是是同一個,直接獲取鎖。sql

②基於數據庫排它鎖實現
仍是使用剛纔建立的那張表,可使用數據庫的排它鎖來實現,基於InnoDB引擎,能夠在獲取鎖時關閉自動提交,而後在查詢語句中加上 for update來獲取數據庫的排它鎖(注意:where後面的字段必定要加索引,只有這樣纔會使用行鎖),當獲取排它鎖成功後,能夠進行操做,操做完畢提交事物來釋放該鎖。數據庫

優點
A: 自然阻塞,在獲取排它鎖失敗時會一直阻塞,知道成功。
B: 服務宕機以後會釋放鎖。
缺點
A: 數據庫單點問題。
B: 鎖的可重入問題。
C: mysql可能會進行查詢優化,可能不使用索引,而去全表掃描,這樣將加表鎖。
D: 若是一個排它鎖一直不去提交,就會佔用數據庫的鏈接,一旦變多就有可能撐爆數據庫鏈接池。緩存

2.基於redis實現。

①最普通的鎖實現
在redis裏面經過下面的指令建立一個key,網絡

// NX   表示只有key不存的時候,纔會成功,存在就失敗     
// PX 30000  表示30秒後鎖自動釋放
SET mylock 隨機值 NX PX 30000

若是建立成功就表示獲取到了鎖,不然就表示沒有獲取到鎖。
釋放鎖就是刪除key,可是通常能夠用lua腳本刪除,這樣能夠去判斷value同樣纔會刪除。session

爲啥要用隨機值呢?由於若是某個客戶端獲取到了鎖,可是阻塞了很長時間才執行完,此時可能已經自動釋放鎖了,此時可能別的客戶端已經獲取到了這個鎖,要是你這個時候直接刪除key的話就會刪除原本不屬於你本身的鎖,因此得用隨機值加上面的lua腳原本釋放鎖。

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

存在的問題
A: redis單點問題,若是redis掛了那麼就會沒法獲取鎖。
B: redis就算是主從架構也會出現問題,好比key還沒來的級複製給從節點,主節點就掛了。

②RedLock算法
獲取鎖原理
假設有一個redis cluster集羣,有5個redis master實例,而後執行以下步驟去獲取鎖。
A: 獲取當前時間戳,單位是毫秒。
B: 輪流嘗試在每一個master節點上面建立鎖,過程比較短,通常就十幾毫秒。
C: 嘗試在大多數節點上創建鎖,好比5個節點就要求最少3個節點建立成功。
D: 客戶端計算創建好鎖的時間,若是創建鎖的時機小於超時時間,就算創建成功了。
E: 若是所創建失敗了,那麼就依次去刪除已經創建成功的鎖。
F: 若是別人已經創建了一把分佈式錯,本身就不斷輪詢去嘗試獲取鎖。

存在問題
A: 獲取鎖失敗時須要不斷去重試,比較消耗性能。
B: 若是redis獲取鎖的客戶端掛掉了,須要等待超時纔會釋放鎖。
C: 上鎖須要遍歷,計算時間等,過程比較複雜。
D: 不是十分的可靠,可參考這兩篇文章:
How to do distributed locking
[Is Redlock safe?](http://antirez.com/news/101)

3.使用zookeeper實現

①基於零時節點實現

zk分佈式鎖,其實能夠作的比較簡單,就是某個節點嘗試建立臨時znode,此時建立成功了就獲取了這個鎖;這個時候別的客戶端來建立鎖會失敗,只能註冊個監聽器監聽這個鎖。釋放鎖就是刪除這個znode,一旦釋放掉就會通知客戶端,而後有一個等待着的客戶端就能夠再次從新加鎖。
簡易代碼以下:

/**
 * ZooKeeperSession
 * @author Administrator
 *
 */
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 {
                         Stat stat = zk.exists(path, true); // 至關因而給node註冊一個監聽器,去看看這個監聽器是否存在
                        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 e) {
                          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 Administrator
     *
     */
    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();  
             }
        }
        
    }
    
    /**
     * 封裝單例的靜態內部類
     * @author Administrator
     *
     */
    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();
    }
    
}

存在的問題
A: 須要動態的去建立、銷燬節點,性能沒有基於redis的高。
B: 使用Zookeeper也有可能帶來併發問題,只是並不常見而已。考慮這樣的狀況,因爲網絡抖動,客戶端可ZK集羣的session鏈接斷了,那麼zk覺得客戶端掛了,就會刪除臨時節點,這時候其餘客戶端就能夠獲取到分佈式鎖了。就可能產生併發問題。這個問題不常見是由於zk有重試機制,一旦zk集羣檢測不到客戶端的心跳,就會重試。屢次重試以後還不行的話纔會刪除臨時節點。
C: 嘗試獲取鎖,鎖銷燬以後須要獲取鎖的線程要去競爭鎖,複雜。

②就行零時順序節點實現。

相好比前面的方式來講。零時順序節點就是在建立零時節點的時候再也不是建立失敗,而是給每一個節點添加了一個編號,每次只有編號最小的能夠獲取到鎖,非最小的節點去監聽離他最近的那個次小的節點,實現獲取鎖的有序性。這樣每一個節點只須要去監聽一個節點,比他小的那個節點釋放鎖他會成功獲取鎖。

簡易代碼實例:

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
        // 假設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);
   
            // 看看剛建立的節點是否是最小的節點
        // locks:10000000000,10000000001,10000000002
            List<String> locks = zk.getChildren(locksRoot, false);
            Collections.sort(locks);
    
            if(lockNode.equals(locksRoot+"/"+ locks.get(0))){
                //若是是最小的節點,則表示取得鎖
                return true;
            }
    
            //若是不是最小的節點,找到比本身小1的節點
            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節點
        // 刪除/locks/10000000001節點
            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);
        }
    }
}

3.比較

三種方案的比較
上面幾種方式,哪一種方式都沒法作到完美。就像CAP同樣,在複雜性、可靠性、性能等方面沒法同時知足,因此,根據不一樣的應用場景選擇最適合本身的纔是王道。

從理解的難易程度角度(從低到高)
數據庫 > 緩存 > Zookeeper

從實現的複雜性角度(從低到高)
Zookeeper >= 緩存 > 數據庫

從性能角度(從高到低)
緩存 > Zookeeper >= 數據庫

從可靠性角度(從高到低) Zookeeper > 緩存 > 數據庫

相關文章
相關標籤/搜索