常見的分佈式鎖詳解

分佈式鎖詳解

分佈式鎖是在分佈式系統中用於控制多個進程對共享資源進行互斥訪問的一種鎖機制。在分佈式系統中,由於進程分佈在不同的服務器上,因此需要一種機制來保證進程之間的互斥訪問。

一、分佈式鎖概述

A. 定義

分佈式鎖是在分佈式系統中,用於控制多個進程對共享資源進行互斥訪問的一種鎖機制。

B. 目的

分佈式鎖的目的是在分佈式系統中實現同步互斥,避免因多個進程同時訪問共享資源而導致資源競爭和數據不一致的問題。

C. 特點

  1. 互斥性:同一時刻只能有一個進程持有鎖。2. 避免死鎖:鎖的設置和釋放都要有固定的規則,避免死鎖的發生。3. 有效性:鎖的設置和釋放必須保證可靠性。4. 速度:鎖的設置和釋放的速度要儘可能快,避免影響業務性能。

D. 應用場景

分佈式鎖常用於以下場景:

  1. 分佈式任務調度 2. 分佈式計數器 3. 分佈式限流器 4. 數據庫事務的分佈式鎖 5. 共享的全局資源的互斥訪問

二、實現分佈式鎖的方式

A. 基於數據庫

在分佈式系統中,可以使用數據庫的樂觀鎖和悲觀鎖來實現分佈式鎖。

1. 樂觀鎖

樂觀鎖基於版本號實現。每次加鎖前,檢查當前版本是否與獲取到鎖時的版本號一致,若一致,則加鎖成功,否則加鎖失敗。

2. 悲觀鎖

悲觀鎖是在加鎖前先獲取資源的鎖,直到操作完成後才釋放鎖。常見的實現方式是數據庫中使用 SELECT...FOR UPDATE 語句來獲取鎖。

B. 基於緩存

在分佈式系統中,由於鎖通常只是臨時的,因此基於緩存實現分佈式鎖的方式具有更高的性能。常用的緩存工具有 Redis 和 ZooKeeper。

1. Redis 實現分佈式鎖

Redis 可以實現分佈式鎖的原理是利用 Redis 的 SET 命令的特性,當且僅當 key 不存在時插入該 key,若 key 已存在,則該命令將返回失敗。利用 SET 命令的特性,可以將 key 作爲鎖的唯一標識,每次加鎖時嘗試插入該 key,若插入成功,則加鎖成功;若插入失敗,則加鎖失敗。同時通過對 key 設置過期時間來防止死鎖的發生。

2. ZooKeeper 實現分佈式鎖

ZooKeeper 實現分佈式鎖的原理是在 ZooKeeper 中創建一個臨時順序節點,每個客戶端獲取鎖時都會先創建一個順序節點,然後獲取所有節點中的最小節點,若該節點爲自己所創建的節點,則加鎖成功;否則,則監聽自己前面的節點,當其他客戶端釋放鎖時,會觸發監聽事件,待自己所監聽的節點被刪除時,則加鎖成功。

三、Redis 分佈式鎖

A. 實現原理

利用 SET 命令的特性,通過將 key 作爲鎖的唯一標識,每次加鎖時嘗試插入該 key,若插入成功,則加鎖成功;若插入失敗,則加鎖失敗。同時通過對 key 設置過期時間來防止死鎖的發生。

B. 使用方法

1. 加鎖

加鎖方法:

public boolean lock(String key, long expire) {
    long expires = System.currentTimeMillis() + expire * 1000 + 1;
    String expiresStr = String.valueOf(expires);
    if (redisTemplate.opsForValue().setIfAbsent(key, expiresStr)) {
        return true;
    }
    String currentValue = redisTemplate.opsForValue().get(key);
    if (currentValue != null && Long.parseLong(currentValue) < System.currentTimeMillis()) {
        String oldValue = redisTemplate.opsForValue().getAndSet(key, expiresStr);
        if (oldValue != null && oldValue.equals(currentValue)) {
            return true;
        }
    }
    return false;
}

該方法實現瞭如下功能:

  1. 判斷是否能夠加鎖(即 key 是否存在),若可以則加鎖成功。2. 若加鎖失敗,則判斷該鎖是否已經超時,若該鎖已經超時,則嘗試重新加鎖。

2. 解鎖

解鎖方法:

public boolean unlock(String key) {
    redisTemplate.opsForValue().getOperations().delete(key);
    return true;
}

該方法實現了將鎖刪除,並釋放鎖的過程。

C. 實現過程中需要考慮的問題

1. 網絡抖動

網絡抖動可能導致加鎖時的結果不一致。因此需要對每次加鎖嘗試的時間進行處理,在加鎖時增加 1 毫秒的時間。

2. 鎖的過期時間

在每次加鎖時,需要給鎖設置過期時間,以防止出現死鎖的情況。過期時間的設置需要根據業務場景的需要進行調整。

3. Redis Master 節點故障

Redis 單節點的故障可能導致鎖的失效,因此可以使用 Redis Sentinel 或者 Redis Cluster 來實現 Redis 的高可用性。

四、ZooKeeper 分佈式鎖

A. 實現原理

在 ZooKeeper 中創建一個臨時順序節點,每個客戶端獲取鎖時都會先創建一個順序節點,然後獲取所有節點中的最小節點,若該節點爲自己所創建的節點,則加鎖成功;否則,則監聽自己前面的節點,當其他客戶端釋放鎖時,會觸發監聽事件,待自己所監聽的節點被刪除時,則加鎖成功。

B. 使用方法

1. 創建鎖節點

創建鎖節點方法:

public void createLockNode(String lockName) {
    try {
        if (zk.exists("/" + lockName, true) == null) {
            zk.create("/" + lockName, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

2. 獲取鎖

獲取鎖方法:

public boolean acquire(String lockName) {
    boolean locked = false;
    try {
        //創建臨時順序節點
        String path = zk.create("/" + lockName + "/lock-", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,
                CreateMode.EPHEMERAL_SEQUENTIAL);
        //獲取所有順序節點
        List<String> list = zk.getChildren("/" + lockName, false);
        //對所有順序節點進行排序
        Collections.sort(list);
        //獲取最小的順序節點
        String minNode = list.get(0);
        //若當前節點是最小的順序節點,則獲取鎖成功
        if (path.equals("/" + lockName + "/" + minNode)) {
            locked = true;
        } else {
            //當前節點不是最小的順序節點,則監聽自己前一位的節點
            String previousNode = list.get(Collections.binarySearch(list, minNode) - 1);
            Stat stat = zk.exists("/" + lockName + "/" + previousNode, true);
            if (stat != null) {
                latch.await();
                locked = true;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return locked;
}

3. 釋放鎖

釋放鎖方法:

public boolean release(String lockName) {
    try {
        String id = Thread.currentThread().getName();
        zk.delete("/" + lockName + "/" + id, -1);
        zk.close();
        return true;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return false;
}

該方法實現了將鎖刪除,並釋放鎖的過程。

五、分佈式鎖的擴展應用

A. 分佈式計數器

分佈式計數器是通過分佈式鎖實現的,每次對計數器進行加 / 減操作前,需要獲取分佈式鎖。

B. 分佈式任務調度

分佈式任務調度是通過對分佈式鎖的維護來實現的。每當某個任務的執行時間到來時,嘗試獲取分佈式鎖,若獲取鎖,則執行任務;否則,等待下一次執行時間。

C. 分佈式限流器

分佈式限流器是通過分佈式鎖和 Redis 原子操作實現的。每次請求過來時,先獲取分佈式鎖,然後根據業務需要修改計數器(如在固定時間段內只允許訪問 10 次),同時設置過期時間,在過期時間到來時自動清零計數器。

六、總結

分佈式鎖是在分佈式系統中用於保證共享資源的互斥訪問的一種鎖機制。分佈式鎖可以基於數據庫或緩存實現,並且常用的緩存工具有 Redis 和 ZooKeeper。分佈式鎖的常見應用場景包括分佈式任務調度、分佈式計數器和分佈式限流器等。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/AsmSqtFLdtQAcXecherxNw