redis實現分散式鎖(包含程式碼以及分析利弊)

煜航發表於2023-02-11

redis實現分散式鎖(基礎版)

使用redis實現分散式鎖的方法有多種,基礎版本是基於setnx命令,即如果不存在則設定。這個命令可以保證只有一個客戶端能夠成功設定一個key,從而獲得鎖。設定key的時候需要設定一個過期時間,以防止死鎖。釋放鎖的時候需要刪除key,或者使用lua指令碼來保證原子性。

//匯入jedis依賴
import redis.clients.jedis.Jedis;

//定義一個分散式鎖的類
class RedisLock {
    //定義一個jedis物件,用於連線redis
    private Jedis jedis;
    //定義一個鎖的key
    private String lockKey;
    //定義一個鎖的過期時間,單位是毫秒
    private long expireTime;

    //構造方法,傳入jedis物件,鎖的key和過期時間
    public RedisLock(Jedis jedis, String lockKey, long expireTime) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.expireTime = expireTime;
    }

    //嘗試獲取鎖的方法,返回一個布林值,表示是否成功獲取鎖
    public boolean tryLock() {
        //使用setnx命令,如果成功設定key,返回1,否則返回0
        long result = jedis.setnx(lockKey, "1");
        //如果返回1,表示獲取鎖成功
        if (result == 1) {
            //設定key的過期時間,防止死鎖
            jedis.pexpire(lockKey, expireTime);
            //返回true
            return true;
        }
        //如果返回0,表示獲取鎖失敗
        else {
            //返回false
            return false;
        }
    }

    //釋放鎖的方法
    public void unlock() {
        //刪除key,釋放鎖
        jedis.del(lockKey);
    }
}

基礎程式碼優缺點:

分析一下這個程式碼的優缺點。這個程式碼的優點是簡單易懂,使用setnx命令可以保證鎖的互斥性,使用過期時間可以防止死鎖。這個程式碼的缺點是不夠健壯,有以下幾個問題:

  • 如果在設定key的過期時間之前,客戶端崩潰或者網路中斷,那麼key可能永遠不會過期,導致其他客戶端無法獲取鎖。
  • 如果在釋放鎖之前,客戶端崩潰或者網路中斷,那麼key可能沒有被刪除,導致其他客戶端無法獲取鎖。
  • 如果在釋放鎖的時候,key已經過期,那麼可能會誤刪其他客戶端設定的key,導致鎖的安全性被破壞。
  • 如果鎖的過期時間太短,那麼可能會導致客戶端在執行任務的過程中,鎖被其他客戶端搶佔,導致任務的一致性被破壞。
  • 如果鎖的過期時間太長,那麼可能會導致客戶端在獲取鎖失敗的情況下,等待的時間過長,導致效能下降。

redis實現分散式鎖(進階版)

為瞭解決這些問題,可以使用一些更復雜的邏輯,如使用lua指令碼來保證設定key和過期時間的原子性,使用唯一的隨機值來標識鎖的持有者,使用續租機制來延長鎖的過期時間等。

//匯入jedis依賴
import redis.clients.jedis.Jedis;

//定義一個分散式鎖的類
class RedisLock {
    //定義一個jedis物件,用於連線redis
    private Jedis jedis;
    //定義一個鎖的key
    private String lockKey;
    //定義一個鎖的過期時間,單位是毫秒
    private long expireTime;
    //定義一個鎖的唯一值,用於標識鎖的持有者
    private String lockValue;
    //定義一個續租執行緒,用於延長鎖的過期時間
    private Thread renewThread;
    //定義一個lua指令碼,用於原子性地設定key和過期時間
    private String setScript = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end";
    //定義一個lua指令碼,用於原子性地刪除key
    private String delScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    //構造方法,傳入jedis物件,鎖的key和過期時間
    public RedisLock(Jedis jedis, String lockKey, long expireTime) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.expireTime = expireTime;
    }

    //嘗試獲取鎖的方法,返回一個布林值,表示是否成功獲取鎖
    public boolean tryLock() {
        //生成一個唯一的隨機值,作為鎖的值
        lockValue = UUID.randomUUID().toString();
        //使用lua指令碼,原子性地設定key和過期時間,如果成功返回1,否則返回0
        long result = (long) jedis.eval(setScript, 1, lockKey, lockValue, String.valueOf(expireTime));
        //如果返回1,表示獲取鎖成功
        if (result == 1) {
            //建立一個續租執行緒,每隔一半的過期時間,就延長鎖的過期時間
            renewThread = new Thread(() -> {
                while (true) {
                    try {
                        //休眠一半的過期時間
                        Thread.sleep(expireTime / 2);
                        //延長鎖的過期時間
                        jedis.pexpire(lockKey, expireTime);
                    } catch (InterruptedException e) {
                        //如果執行緒被中斷,退出迴圈
                        break;
                    }
                }
            });
            //啟動續租執行緒
            renewThread.start();
            //返回true
            return true;
        }
        //如果返回0,表示獲取鎖失敗
        else {
            //返回false
            return false;
        }
    }

    //釋放鎖的方法
    public void unlock() {
        //使用lua指令碼,原子性地刪除key,只有當key的值和鎖的值相等時,才會刪除
        jedis.eval(delScript, 1, lockKey, lockValue);
        //中斷續租執行緒
        renewThread.interrupt();
    }
}

進階程式碼優缺點:

分析一下這個程式碼的優缺點。這個程式碼的優點是比之前的程式碼更健壯,解決了以下幾個問題:

  • 使用lua指令碼可以保證設定key和過期時間的原子性,避免了客戶端崩潰或者網路中斷導致的死鎖。
  • 使用唯一的隨機值可以標識鎖的持有者,避免了誤刪其他客戶端設定的key的情況。
  • 使用續租機制可以延長鎖的過期時間,避免了鎖被其他客戶端搶佔的情況。
  • 使用lua指令碼可以保證刪除key的原子性,避免了客戶端崩潰或者網路中斷導致的鎖未釋放的情況。

這個程式碼的缺點是還是有一些問題,如:

  • 如果續租執行緒出現異常或者延遲,那麼鎖可能會過期,導致鎖的安全性被破壞。
  • 如果鎖的過期時間太長,那麼可能會導致客戶端在獲取鎖失敗的情況下,等待的時間過長,導致效能下降。
  • 如果鎖的過期時間太短,那麼可能會導致續租執行緒頻繁地延長鎖的過期時間,導致網路開銷增加。
  • 如果redis伺服器出現故障或者主從切換,那麼鎖的狀態可能會丟失,導致鎖的一致性被破壞。

為瞭解決這些問題,可以使用一些更復雜的邏輯,如使用watchdog機制來監控續租執行緒的狀態,使用自旋鎖或者阻塞鎖來最佳化鎖的等待策略,使用叢集或者哨兵模式來提高redis的可用性等。或者可以使用redisson框架,它已經實現了這些邏輯,而且提供了更多的分散式鎖的功能和選項。
但是進階版程式碼已經能cover大部分的場景,沒有技術能實現萬無一失,只是在出現問題的時候進行有效的補救,代價在承受範圍內就行。也沒有什麼技術是永恆最好的,拋開業務談方案就像空中樓閣。
最後,再一次感謝大家的閱讀!

相關文章