分散式鎖最終解決方案是RedLock嗎?為什麼?

2024.01.09

RedLock 是Redis 分散式鎖定的實作方案,由Redis 的作者Salvatore Sanfilippo 提出。

RedLock 演算法旨在解決單一Redis 實例作為分散式鎖定時可能出現的單點故障問題,透過在多個獨立運行的Redis 實例上同時取得鎖的方式來提高鎖服務的可用性和安全性。

1.實現思路

RedLock 是對叢集的每個節點進行加鎖,如果大多數節點(N/2+1)加鎖成功,則才會認為加鎖成功。這樣即使叢集中有某個節點掛掉了,因為大部分叢集節點都加鎖成功了,所以分散式鎖還是可以繼續使用的。

2、實現程式碼

在Java 開發中,可以使用Redisson 框架很方便的實作RedLock,具體操作程式碼如下:

import org.redisson.Redisson;
import org.redisson.api.RedisClient;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.redisson.RedissonRedLock;

public class RedLockDemo {

    public static void main(String[] args) {
        // 创建 Redisson 客户端配置
        Config config = new Config();
        config.useClusterServers()
        .addNodeAddress("redis://127.0.0.1:6379",
                        "redis://127.0.0.1:6380",
                        "redis://127.0.0.1:6381"); // 假设有三个 Redis 节点
        // 创建 Redisson 客户端实例
        RedissonClient redissonClient = Redisson.create(config);
        // 创建 RedLock 对象
        RedissonRedLock redLock = redissonClient.getRedLock("resource");
        try {
            // 尝试获取分布式锁,最多尝试 5 秒获取锁,并且锁的有效期为 5000 毫秒
            boolean lockAcquired = redLock.tryLock(5, 5000, TimeUnit.MILLISECONDS); 
            if (lockAcquired) {
                // 加锁成功,执行业务代码...
            } else {
                System.out.println("Failed to acquire the lock!");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("Interrupted while acquiring the lock");
        } finally {
            // 无论是否成功获取到锁,在业务逻辑结束后都要释放锁
            if (redLock.isLocked()) {
                redLock.unlock();
            }
            // 关闭 Redisson 客户端连接
            redissonClient.shutdown();
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.

3、實現原理

Redisson 中的RedLock 是基於RedissonMultiLock(連鎖)實現的。

RedissonMultiLock 是Redisson 提供的分散式鎖定類型,它可以同時操作多個鎖,以達到對多個鎖進行統一管理的目的。聯鎖的操作是原子性的,即要么全部鎖住,要么全部解鎖。這樣可以保證多個鎖的一致性。

RedissonMultiLock 使用範例如下:

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.multi.MultiLock;

public class RedissonMultiLockDemo {

    public static void main(String[] args) throws InterruptedException {
        // 创建 Redisson 客户端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);

        // 创建多个分布式锁实例
        RLock lock1 = redisson.getLock("lock1");
        RLock lock2 = redisson.getLock("lock2");
        RLock lock3 = redisson.getLock("lock3");

        // 创建 RedissonMultiLock 对象
        MultiLock multiLock = new MultiLock(lock1, lock2, lock3);

        // 加锁
        multiLock.lock();
        try {
            // 执行任务
            System.out.println("Lock acquired. Task started.");
            Thread.sleep(3000);
            System.out.println("Task finished. Releasing the lock.");
        } finally {
            // 释放锁
            multiLock.unlock();
        }
        // 关闭客户端连接
        redisson.shutdown();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.

在範例中,我們首先建立了一個Redisson 用戶端並連接到Redis 伺服器。然後,我們使用redisson.getLock 方法建立了多個分散式鎖定實例。接下來,我們透過傳入這些鎖定實例來建立了RedissonMultiLock 物件。

說回正題,RedissonRedLock 是基於RedissonMultiLock 實現的這一點,可以從繼承關係看出。

RedissonRedLock 繼承自RedissonMultiLock,核心實作原始碼如下:

public class RedissonRedLock extends RedissonMultiLock {
    public RedissonRedLock(RLock... locks) {
        super(locks);
    }

    /**
     * 锁可以失败的次数,锁的数量-锁成功客户端最小的数量
     */
    @Override
    protected int failedLocksLimit() {
        return locks.size() - minLocksAmount(locks);
    }

    /**
     * 锁的数量 / 2 + 1,例如有3个客户端加锁,那么最少需要2个客户端加锁成功
     */
    protected int minLocksAmount(final List<RLock> locks) {
        return locks.size()/2 + 1;
    }

    /** 
     * 计算多个客户端一起加锁的超时时间,每个客户端的等待时间
     */
    @Override
    protected long calcLockWaitTime(long remainTime) {
        return Math.max(remainTime / locks.size(), 1);
    }

    @Override
    public void unlock() {
        unlockInner(locks);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.

從上述原始碼可以看出,Redisson 中的RedLock 是基於RedissonMultiLock(連鎖)實現的,當RedLock 是對叢集的每個節點進行加鎖,如果大多數節點,也就是N/2+1 個節點加鎖成功,則認為RedLock 加鎖成功。

4.存在問題

RedLock 主要存在以下兩個問題:

  • 效能問題:RedLock 要等待大多數節點返回之後,才能加鎖成功,而這個過程中可能會因為網路問題,或是節點逾時的問題,影響加鎖的效能。
  • 並發安全性問題:當客戶端加鎖時,如果遇到GC 可能會導致加鎖失效,但GC 後誤認為加鎖成功的安全事故,例如以下流程:
  1. 客戶端A 請求3 個節點進行加鎖。
  2. 在節點回復處理之前,客戶端A 進入GC 階段(存在STW,全域停頓)。
  3. 之後因為加鎖時間的原因,鎖已經失效了。
  4. 客戶端B 請求加鎖(和客戶端A 是同一把鎖),加鎖成功。
  5. 客戶端A GC 完成,繼續處理前面節點的訊息,誤以為加鎖成功。
  6. 此時客戶端B 和客戶端A 同時加鎖成功,出現並發安全性問題。

5.已廢棄RedLock

因為RedLock 存在的問題爭議較大,且沒有完美的解決方案,所以Redisson 中已經廢棄了RedLock,這一點在Redisson 官方文件中能找到,如下圖所示:

6.廢棄RedLock 後的解決方案

雖然Redisson 中已經廢棄了RedLock,但是你可以直接使用Redisson 中的普通的加鎖即可,因為它的普通鎖會基於wait 機制,等待鎖將資訊同步到從節點,從而保證資料一致性的,雖然無法完全避免資料一致性問題,但也能最大限度的保證資料的一致性。

課後思考

既然普通的分散式鎖存在單點問題?而RedLock 又不是最完美的解決方案,那麼在分散式鎖定領域,誰才是最終的解決方案呢?請在評論區留下您的解決方案,以及對應的原因?