分散式鎖的3種實現!附程式碼

2023-09-13 12:01:37

分散式鎖是一種用於保證分散式系統中多個程序或執行緒同步存取共用資源的技術。同時它又是面試中的常見問題,所以我們本文就重點來看分散式鎖的具體實現(含實現程式碼)。

在分散式系統中,由於各個節點之間的網路通訊延遲、故障等原因,可能會導致資料不一致的問題。分散式鎖通過協調多個節點的行為,保證在任何時刻只有一個節點可以存取共用資源,以避免資料的不一致性和衝突。

1.分散式鎖要求

分散式鎖通常需要滿足以下幾個要求:

  1. 互斥性:在任意時刻只能有一個使用者端持有鎖。
  2. 不會發生死鎖:即使持有鎖的使用者端發生故障,也能保證鎖最終會被釋放。
  3. 具有容錯性:分散式鎖需要能夠容忍節點故障等異常情況,保證系統的穩定性。

2.實現方案

在 Java 中,實現分散式鎖的方案有多種,包括:

  1. 基於資料庫實現的分散式鎖:可以通過資料庫的樂觀鎖或悲觀鎖實現分散式鎖,但是由於資料庫的 IO 操作比較慢,不適合高並行場景。
  2. 基於 ZooKeeper 實現的分散式鎖:ZooKeeper 是一個高可用性的分散式協調服務,可以通過它來實現分散式鎖。但是使用 ZooKeeper 需要部署額外的服務,增加了系統複雜度。
  3. 基於 Redis 實現的分散式鎖:Redis 是一個高效能的記憶體資料庫,支援分散式部署,可以通過Redis的原子操作實現分散式鎖,而且具有高效能和高可用性。

3.資料庫分散式鎖

資料庫的樂觀鎖或悲觀鎖都可以實現分散式鎖,下面分別來看。

3.1 悲觀鎖

在資料庫中使用 for update 關鍵字可以實現悲觀鎖,我們在 Mapper 中新增 for update 即可對資料加鎖,實現程式碼如下:

<!-- UserMapper.xml -->
<select id="selectByIdForUpdate" resultType="User">
    SELECT * FROM user WHERE id = #{id} FOR UPDATE
</select>

在 Service 中呼叫 Mapper 方法,即可獲取到加鎖的資料:

@Transactional
public void updateWithPessimisticLock(int id, String name) {
    User user = userMapper.selectByIdForUpdate(id);
    if (user != null) {
        user.setName(name);
        userMapper.update(user);
    } else {
        throw new RuntimeException("資料不存在");
    }
}

3.2 樂觀鎖

在 MyBatis 中,可以通過給表新增一個版本號欄位來實現樂觀鎖。在 Mapper 中,使用 標籤定義更新語句,同時使用 set 標籤設定版本號的增量。

<!-- UserMapper.xml -->
<update id="updateWithOptimisticLock">
    UPDATE user SET
    name = #{name},
    version = version + 1
    WHERE id = #{id} AND version = #{version}
</update>

在 Service 中呼叫 Mapper 方法,需要傳入更新資料的版本號。如果更新失敗,說明資料已經被其他事務修改,具體實現程式碼如下:

@Transactional
public void updateWithOptimisticLock(int id, String name, int version) {
    User user = userMapper.selectById(id);
    if (user != null) {
        user.setName(name);
        user.setVersion(version);
        int rows = userMapper.updateWithOptimisticLock(user);
        if (rows == 0) {
            throw new RuntimeException("資料已被其他事務修改");
        }
    } else {
        throw new RuntimeException("資料不存在");
    }
}

4.Zookeeper 分散式鎖

在 Spring Boot 中,可以使用 Curator 框架來實現 ZooKeeper 分散式鎖,具體實現分為以下 3 步:

  1. 引入 Curator 和 ZooKeeper 使用者端依賴;
  2. 設定 ZooKeeper 連線資訊;
  3. 編寫分散式鎖實現類。

4.1 引入 Curator 和 ZooKeeper

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>latest</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>latest</version>
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>latest</version>
</dependency>

4.2 設定 ZooKeeper 連線

在 application.yml 中新增 ZooKeeper 連線設定:

spring:
  zookeeper:
    connect-string: localhost:2181
    namespace: demo

4.3 編寫分散式鎖實現類

@Component
public class DistributedLock {

    @Autowired
    private CuratorFramework curatorFramework;

    /**
     * 獲取分散式鎖
     *
     * @param lockPath   鎖路徑
     * @param waitTime   等待時間
     * @param leaseTime  鎖持有時間
     * @param timeUnit   時間單位
     * @return 鎖物件
     * @throws Exception 獲取鎖異常
     */
    public InterProcessMutex acquire(String lockPath, long waitTime, long leaseTime, TimeUnit timeUnit) throws Exception {
        InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);
        if (!lock.acquire(waitTime, timeUnit)) {
            throw new RuntimeException("獲取分散式鎖失敗");
        }
        if (leaseTime > 0) {
            lock.acquire(leaseTime, timeUnit);
        }
        return lock;
    }

    /**
     * 釋放分散式鎖
     *
     * @param lock 鎖物件
     * @throws Exception 釋放鎖異常
     */
    public void release(InterProcessMutex lock) throws Exception {
        if (lock != null) {
            lock.release();
        }
    }
}

5.Redis 分散式鎖

我們可以使用 Redis 使用者端 Redisson 實現分散式鎖,它的實現步驟如下:

  1. 新增 Redisson 依賴
  2. 設定 Redisson 連線資訊
  3. 編寫分散式鎖程式碼類

5.1 新增 Redisson 依賴

在 pom.xml 中新增如下設定:

<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.20.0</version>
</dependency>

5.2 設定 Redisson 連線

在 Spring Boot 專案的組態檔 application.yml 中新增 Redisson 設定:

spring:
  data:
    redis:
      host: localhost
      port: 6379
      database: 0
redisson:
  codec: org.redisson.codec.JsonJacksonCodec
  single-server-config:
    address: "redis://${spring.data.redis.host}:${spring.redis.port}"
    database: "${spring.data.redis.database}"
    password: "${spring.data.redis.password}"

5.3 編寫分散式鎖程式碼類

import jakarta.annotation.Resource;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class RedissonLockService {
    @Resource
    private Redisson redisson;

    /**
     * 加鎖
     *
     * @param key     分散式鎖的 key
     * @param timeout 超時時間
     * @param unit    時間單位
     * @return
     */
    public boolean tryLock(String key, long timeout, TimeUnit unit) {
        RLock lock = redisson.getLock(key);
        try {
            return lock.tryLock(timeout, unit);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

    /**
     * 釋放分散式鎖
     *
     * @param key 分散式鎖的 key
     */
    public void unlock(String key) {
        RLock lock = redisson.getLock(key);
        lock.unlock();
    }
}

6.Redis VS Zookeeper

Redis 和 ZooKeeper 都可以用來實現分散式鎖,它們在實現分散式鎖的機制和原理上有所不同,具體區別如下:

  1. 資料儲存方式:Redis 將鎖資訊儲存在記憶體中,而 ZooKeeper 將鎖資訊儲存在 ZooKeeper 的節點上,因此 ZooKeeper 需要更多的磁碟空間。
  2. 鎖的釋放:Redis 的鎖是通過設定鎖的過期時間來自動釋放的,而 ZooKeeper 的鎖需要手動釋放,如果鎖的持有者出現宕機或網路中斷等情況,需要等待鎖的超時時間才能自動釋放。
  3. 鎖的競爭機制:Redis 使用的是單機鎖,即所有請求都直接連線到同一臺 Redis 伺服器,容易發生單點故障;而 ZooKeeper 使用的是分散式鎖,即所有請求都連線到 ZooKeeper 叢集,具有較好的可用性和可延伸性。
  4. 一致性:Redis 的鎖是非嚴格意義下的分散式鎖,因為在多臺機器上執行多個程序時,由於 Redis 的主從同步可能會存在資料不一致的問題;而 ZooKeeper 是強一致性的分散式系統,保證了資料的一致性。
  5. 效能:Redis 的效能比 ZooKeeper 更高,因為 Redis 將鎖資訊儲存在記憶體中,而 ZooKeeper 需要進行磁碟讀寫操作。

總之,Redis 適合實現簡單的分散式鎖場景,而 ZooKeeper 適合實現複雜的分散式協調場景,也就是 ZooKeeper 適合強一致性的分散式系統。

強一致性是指系統中的所有節點在任何時刻看到的資料都是一致的。ZooKeeper 中的資料是有序的樹形結構,每個節點都有唯一的路徑識別符號,所有節點都共用同一份資料,當任何一個節點對資料進行修改時,所有節點都會收到通知,更新資料,並確保資料的一致性。
在 ZooKeeper 中,強一致性體現在資料的讀寫操作上。ZooKeeper 使用 ZAB(ZooKeeper Atomic Broadcast)協定來保證資料的一致性,該協定確保了資料更新的順序,所有的資料更新都需要經過叢集中的大多數節點確認,保證了資料的一致性和可靠性。

小結

在 Java 中,使用資料庫、ZooKeeper 和 Redis 都可以實現分散式鎖。但資料庫 IO 操作比較慢,不適合高並行場景;Redis 執行效率最高,但在主從切換時,可能會出現鎖丟失的情況;ZooKeeper 是一個高可用性的分散式協調服務,可以保證資料的強一致性,但是使用 ZooKeeper 需要部署額外的服務,增加了系統複雜度。所以沒有最好的解決方案,只有最合適自己的解決方案。

本文已收錄到我的面試小站 www.javacn.site,其中包含的內容有:Redis、JVM、並行、並行、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、設計模式、訊息佇列等模組。