使用Redis實現分佈式鎖詳解

在講分佈式鎖之呢,我們不妨先來說說什麼是分佈式系統。
在這裏插入圖片描述
在系統早期,用戶量少,可能我們一個app的所有模塊都存在與一個應用包,部署在一臺機器上,這便是我們的單體應用架構。這種設計,如果用戶訪問量大,便很容易造成系統壓力過大而導致的系統宕機,其次如果一個模塊,比如支付功能bug或其他原因,便直接導致整個系統癱瘓。
爲解決這個問題,可能會想到我們的集羣部署。
在這裏插入圖片描述
集羣部署配合配合負載均衡(負載算法),可以在很大程度上減少單臺服務器訪問壓力,但是這種模式任然存在某一模塊bug,導致整個應用掛死的現象,繼而導致整個集羣架構癱瘓。
繼而分佈式架構便應聲而出,分佈式架構有一個最大的好處便是可以防止我麼的某一模塊的癱瘓導致整個應用的掛死。
在這裏插入圖片描述
如圖,就算我們的支付模塊掛了,我們還是可以完成我們的登錄,下單等其他操作,不會造成整個應用羣宕機。

當然今天主要是講我們的分佈式鎖的實現。對於如上的分佈式架構,會有一個什麼問題呢?見下圖
在這裏插入圖片描述
如圖,如果我們系統是分佈式架構,舉個簡單例子現在又庫存模塊和訂單模塊,如果庫存只有1,但是此時同時有兩個用戶下單成功,都同時進入庫存去做減庫存操作,便會出現併發問題。我們可以用簡單的代碼來模擬這個操作。
在這裏插入圖片描述
在這裏插入圖片描述
如圖,新建order類,模擬我們的訂單系統,stock類,模擬我們的庫存操作,庫存只有一個,顯然只有STOCK_NUM>0的時候纔可以減庫存成功。
在這裏插入圖片描述
新增測試類,模擬用戶操作,期間起兩個線程,模擬兩個用戶同時操作,下單後進行減庫存,顯然出現了併發問題,兩個用戶都減庫存成功。那麼這個問題要怎麼解決呢,於是便引入我們今天的主角了,分佈式鎖了。

public class RedisLock implements Lock {

    ThreadLocal<Jedis> jedis = new ThreadLocal<Jedis>();
    private static String LOCK_NAME = "LOCK";
    private static String REQUEST_ID = "111111";

    void init(){
        if (jedis.get() ==null)
            jedis.set(new Jedis("localhost"));
    }

    public void lock() {
        init();
        if (tryLock())
            jedis.get().set(LOCK_NAME,REQUEST_ID,"NX","PX",5000);
    }

    public boolean tryLock() {
        while(true){
            Long ret = jedis.get().setnx(LOCK_NAME,REQUEST_ID);
            if (ret==1)
                return true;
        }
    }

    public void unlock() {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        jedis.get().eval(script, Collections.singletonList(LOCK_NAME), Collections.singletonList(REQUEST_ID));
    }

    public Condition newCondition() {
        return null;
    }

    public void lockInterruptibly() throws InterruptedException {

    }

    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

}

代碼如上,既然是鎖,當然首先需要實現我們的Lock接口,當然對於裏面的方法,我們只需要實現3個方法即可:tryLock(),lock()和unlock()即可。

 public void lock() {
        init();
        if (tryLock())
            jedis.get().set(LOCK_NAME,REQUEST_ID,"NX","PX",5000);
    }

對於lock方法,很簡單,都是固定套路的,先嚐試拿鎖,如果能拿到鎖,則往下執行,否則阻塞,在設置key的時候,設置一個超時時間,防止系統死鎖問題。

jedis.set(LOCK_NAME, REQUEST_ID);
jedis.expire(LOCK_NAME, 3000);

有人說是不是可以用上述寫法,其實這種寫法是有問題的,就不是原子操作了,如果在
jedis.set(LOCK_NAME, REQUEST_ID);執行完之後,系統掛死了,超時時間設置不了,後續解鎖操作當然也沒法執行了,則系統便一直死鎖了。

考慮併發問題,對於jedis連接,採用ThreadLocal去構造。

ThreadLocal<Jedis> jedis = new ThreadLocal<Jedis>();
void init(){
    if (jedis.get() ==null)
        jedis.set(new Jedis("localhost"));
}

對於tryLock(),我們可以使用一個while(true)循環,去一直嘗試拿鎖,如果拿到,則返回結果標識

 public boolean tryLock() {
        while(true){
            Long ret = jedis.get().setnx(LOCK_NAME,REQUEST_ID);
            if (ret==1)
                return true;
        }
    }

可能有人會問,爲什麼這裏不用 jedis.get().get(LOCK_NAME)去判斷,這裏可以想一下,如果我們兩個線程同時執行到tryLock(),但是沒有執行lock(),redis裏面沒有對應的key,這時會出現兩個tryLock().get(LOCK_NAME)的值都是false,然後導致兩個線程都可以拿到鎖的現象,導致線程安全問題,但是用setnx就不會。會先去判斷key是否存在然後去設置,所有這時候肯定不會出現相關問題。

對於unlock()操作

  public void unlock() {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        jedis.get().eval(script, Collections.singletonList(LOCK_NAME), Collections.singletonList(REQUEST_ID));
    }

當然對於unlock()操作,也是一樣的,爲了保證原子性操作,採用腳本方式去執行操作,保證原子性。當然這裏的原子性還是爲了防止宕機操作,如果執行到unlock(),del操作之前系統掛死了,則這個key就不會被刪除了,便會導致系統死鎖問題。

 String value = jedis.get(LOCK_NAME);
        if (REQUEST_ID.equals(value)) {
           jedis.del(LOCK_NAME);
       }

上述這種寫法也是有問題的,以後寫redis分佈式鎖的時候還需注意。
在這裏插入圖片描述

至此,我們一個redis的分佈式鎖就寫好了,一直執行也不會有線程安全問題了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章